diff --git a/Pachyderm/Model/Account.swift b/Pachyderm/Model/Account.swift index 9540d6a2..ca7f1214 100644 --- a/Pachyderm/Model/Account.swift +++ b/Pachyderm/Model/Account.swift @@ -22,13 +22,47 @@ public class Account: Decodable { public let url: URL public let avatar: URL public let avatarStatic: URL - public let header: URL - public let headerStatic: URL + public let header: URL? + public let headerStatic: URL? public private(set) var emojis: [Emoji] public let moved: Bool? public let fields: [Field]? public let bot: Bool? + // we need a custom decoder, because all API-compatible implementations don't return some data in the same format + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.username = try container.decode(String.self, forKey: .username) + self.acct = try container.decode(String.self, forKey: .acct) + self.displayName = try container.decode(String.self, forKey: .displayName) + self.locked = try container.decode(Bool.self, forKey: .locked) + self.createdAt = try container.decode(Date.self, forKey: .createdAt) + self.followersCount = try container.decode(Int.self, forKey: .followersCount) + self.followingCount = try container.decode(Int.self, forKey: .followingCount) + self.statusesCount = try container.decode(Int.self, forKey: .statusesCount) + self.note = try container.decode(String.self, forKey: .note) + self.url = try container.decode(URL.self, forKey: .url) + self.avatar = try container.decode(URL.self, forKey: .avatar) + self.avatarStatic = try container.decode(URL.self, forKey: .avatarStatic) + if let header = try? container.decodeIfPresent(String.self, forKey: .header), + let url = URL(string: header) { + self.header = url + } else { + self.header = nil + } + if let headerStatic = try? container.decodeIfPresent(String.self, forKey: .headerStatic), + let url = URL(string: headerStatic) { + self.headerStatic = url + } else { + self.headerStatic = nil + } + self.emojis = try container.decode([Emoji].self, forKey: .emojis) + self.moved = try container.decodeIfPresent(Bool.self, forKey: .moved) + self.fields = try container.decodeIfPresent([Field].self, forKey: .fields) + self.bot = try container.decodeIfPresent(Bool.self, forKey: .bot) + } + public static func authorizeFollowRequest(_ account: Account) -> Request { return Request(method: .post, path: "/api/v1/follow_requests/\(account.id)/authorize") } diff --git a/Pachyderm/Model/Attachment.swift b/Pachyderm/Model/Attachment.swift index b9663bc6..bb411324 100644 --- a/Pachyderm/Model/Attachment.swift +++ b/Pachyderm/Model/Attachment.swift @@ -64,6 +64,17 @@ extension Attachment { case gifv case audio case unknown + + // we need a custom decoder, because all API-compatible implementations don't return some data in the same format + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let str = try container.decode(String.self) + if let kind = Kind(rawValue: str.lowercased()) { + self = kind + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Attachment type must be one of image, video, gifv, audio, or unknown.") + } + } } } diff --git a/Pachyderm/Model/LoginSettings.swift b/Pachyderm/Model/LoginSettings.swift index 096345e4..3f76388d 100644 --- a/Pachyderm/Model/LoginSettings.swift +++ b/Pachyderm/Model/LoginSettings.swift @@ -10,9 +10,10 @@ import Foundation public class LoginSettings: Decodable { public let accessToken: String - private let scope: String + private let scope: String? public var scopes: [Scope] { + guard let scope = scope else { return [] } return scope.components(separatedBy: .whitespaces).compactMap(Scope.init) } diff --git a/Pachyderm/Model/RegisteredApplication.swift b/Pachyderm/Model/RegisteredApplication.swift index c0db4ed0..063465dc 100644 --- a/Pachyderm/Model/RegisteredApplication.swift +++ b/Pachyderm/Model/RegisteredApplication.swift @@ -13,6 +13,27 @@ public class RegisteredApplication: Decodable { public let clientID: String public let clientSecret: String + + // we need a custom decoder, because all API-compatible implementations don't return some data in the same format + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let id = try? container.decode(String.self, forKey: .id) { + self.id = id + } else if let id = try? container.decode(Int.self, forKey: .id) { + self.id = String(id) + } else { + throw DecodingError.dataCorruptedError(forKey: CodingKeys.id, in: container, debugDescription: "Expect application id to be string or number") + } + if let clientID = try? container.decode(String.self, forKey: .clientID) { + self.clientID = clientID + } else if let clientID = try? container.decode(Int.self, forKey: .clientID) { + self.clientID = String(clientID) + } else { + throw DecodingError.dataCorruptedError(forKey: CodingKeys.id, in: container, debugDescription: "Expect client id to be string or number") + } + self.clientSecret = try container.decode(String.self, forKey: .clientSecret) + } + private enum CodingKeys: String, CodingKey { case id case clientID = "client_id" diff --git a/Pachyderm/Model/Status.swift b/Pachyderm/Model/Status.swift index 6f2b7587..29562d8d 100644 --- a/Pachyderm/Model/Status.swift +++ b/Pachyderm/Model/Status.swift @@ -27,7 +27,7 @@ public class Status: Decodable { public let favourited: Bool? public let muted: Bool? public let sensitive: Bool - public let spoilerText: String + public let spoilerText: String? public let visibility: Visibility public let attachments: [Attachment] public let mentions: [Mention] diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 6bdd9cfa..745157a4 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -131,13 +131,14 @@ class ComposeViewController: UIViewController { contentWarningEnabled = false contentWarningContainerView.isHidden = true } else { - contentWarningEnabled = !inReplyTo.spoilerText.isEmpty + contentWarningEnabled = inReplyTo.spoilerText != nil && !inReplyTo.spoilerText!.isEmpty contentWarningContainerView.isHidden = !contentWarningEnabled if Preferences.shared.contentWarningCopyMode == .prependRe, - !inReplyTo.spoilerText.lowercased().starts(with: "re:") { - contentWarningTextField.text = "re: \(inReplyTo.spoilerText)" + let spoiler = inReplyTo.spoilerText, + !spoiler.lowercased().starts(with: "re:") { + contentWarningTextField.text = "re: \(spoiler)" } else { - contentWarningTextField.text = inReplyTo.spoilerText + contentWarningTextField.text = inReplyTo.spoilerText != nil ? inReplyTo.spoilerText : "" } } diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index 3d9fdda4..bd1e9732 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -63,11 +63,14 @@ class ProfileHeaderTableViewCell: UITableViewCell { self.avatarURL = nil } } - ImageCache.headers.get(account.header) { (data) in - guard let data = data else { return } - DispatchQueue.main.async { - self.headerImageView.image = UIImage(data: data) - self.headerURL = nil + self.headerImageView.image = nil + if let header = account.header { + ImageCache.headers.get(header) { (data) in + guard let data = data else { return } + DispatchQueue.main.async { + self.headerImageView.image = UIImage(data: data) + self.headerURL = nil + } } } diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index b353b4ac..4a700a64 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -126,10 +126,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell { contentLabel.statusID = statusID - collapsible = !status.spoilerText.isEmpty + collapsible = status.spoilerText != nil && !status.spoilerText!.isEmpty setCollapsed(collapsible, animated: false) contentWarningLabel.text = status.spoilerText - contentWarningLabel.isHidden = status.spoilerText.isEmpty + contentWarningLabel.isHidden = status.spoilerText != nil && status.spoilerText!.isEmpty } private func updateStatusState(status: Status) { diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index 59ed1b6d..9443036c 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -135,10 +135,10 @@ class StatusTableViewCell: UITableViewCell { contentLabel.statusID = status.id - collapsible = !status.spoilerText.isEmpty + collapsible = status.spoilerText != nil && !status.spoilerText!.isEmpty setCollapsed(collapsible, animated: false) contentWarningLabel.text = status.spoilerText - contentWarningLabel.isHidden = status.spoilerText.isEmpty + contentWarningLabel.isHidden = status.spoilerText != nil && status.spoilerText!.isEmpty } private func updateStatusState(status: Status) { diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 4494f288..70d66cbd 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -263,7 +263,7 @@ struct XCBActions { "following": account.followingCount.description, "url": account.url.absoluteString, "avatarURL": account.avatar.absoluteString, - "headerURL": account.header.absoluteString + "headerURL": account.header?.absoluteString ]) } } @@ -278,7 +278,7 @@ struct XCBActions { "following": account.followingCount.description, "url": account.url.absoluteString, "avatarURL": account.avatar.absoluteString, - "headerURL": account.header.absoluteString + "headerURL": account.header?.absoluteString ]) }