From 90bc9b91de906202437b4158d537ad1733beb04d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 6 May 2020 18:40:12 -0400 Subject: [PATCH] Add AccountProtocol and StatusProtocol Provides a single interfaces for API and CoreData statuses and accounts --- Pachyderm/Model/Account.swift | 6 +-- .../Model/Protocols/AccountProtocol.swift | 33 ++++++++++++++++ .../Model/Protocols/StatusProtocol.swift | 38 +++++++++++++++++++ Pachyderm/Model/Status.swift | 8 ++-- Tusker.xcodeproj/project.pbxproj | 16 ++++++++ Tusker/CoreData/AccountMO.swift | 19 ++++++---- Tusker/CoreData/StatusMO.swift | 37 +++++++++--------- .../Tusker.xcdatamodel/contents | 15 ++++---- Tusker/LazilyDecoding.swift | 6 +-- Tusker/TuskerNavigationDelegate.swift | 6 ++- .../Status/BaseStatusTableViewCell.swift | 4 +- .../ConversationMainStatusTableViewCell.swift | 2 +- .../Status/TimelineStatusTableViewCell.swift | 2 +- 13 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 Pachyderm/Model/Protocols/AccountProtocol.swift create mode 100644 Pachyderm/Model/Protocols/StatusProtocol.swift diff --git a/Pachyderm/Model/Account.swift b/Pachyderm/Model/Account.swift index 66b2d9cb..c8343bd1 100644 --- a/Pachyderm/Model/Account.swift +++ b/Pachyderm/Model/Account.swift @@ -8,7 +8,7 @@ import Foundation -public class Account: Decodable { +public final class Account: AccountProtocol, Decodable { public let id: String public let username: String public let acct: String @@ -27,7 +27,7 @@ public class Account: Decodable { public private(set) var emojis: [Emoji] public let moved: Bool? public let movedTo: Account? - public let fields: [Field]? + public let fields: [Field] public let bot: Bool? public required init(from decoder: Decoder) throws { @@ -49,7 +49,7 @@ public class Account: Decodable { self.header = try container.decode(URL.self, forKey: .header) self.headerStatic = try container.decode(URL.self, forKey: .url) self.emojis = try container.decode([Emoji].self, forKey: .emojis) - self.fields = try? container.decode([Field].self, forKey: .fields) + self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? [] self.bot = try? container.decode(Bool.self, forKey: .bot) if let moved = try? container.decode(Bool.self, forKey: .moved) { diff --git a/Pachyderm/Model/Protocols/AccountProtocol.swift b/Pachyderm/Model/Protocols/AccountProtocol.swift new file mode 100644 index 00000000..020d4b45 --- /dev/null +++ b/Pachyderm/Model/Protocols/AccountProtocol.swift @@ -0,0 +1,33 @@ +// +// AccountProtocol.swift +// Pachyderm +// +// Created by Shadowfacts on 4/11/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import Foundation + +public protocol AccountProtocol { + associatedtype Account: AccountProtocol + + var id: String { get } + var username: String { get } + var acct: String { get } + var displayName: String { get } + var locked: Bool { get } + var createdAt: Date { get } + var followersCount: Int { get } + var followingCount: Int { get } + var statusesCount: Int { get } + var note: String { get } + var url: URL { get } + var avatar: URL { get } + var header: URL { get } + var moved: Bool? { get } + var bot: Bool? { get } + + var movedTo: Account? { get } + var emojis: [Emoji] { get } + var fields: [Pachyderm.Account.Field] { get } +} diff --git a/Pachyderm/Model/Protocols/StatusProtocol.swift b/Pachyderm/Model/Protocols/StatusProtocol.swift new file mode 100644 index 00000000..345b20c5 --- /dev/null +++ b/Pachyderm/Model/Protocols/StatusProtocol.swift @@ -0,0 +1,38 @@ +// +// StatusProtocol.swift +// Pachyderm +// +// Created by Shadowfacts on 4/11/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import Foundation + +public protocol StatusProtocol { + associatedtype Status: StatusProtocol + associatedtype Account: AccountProtocol + + var id: String { get } + var uri: String { get } + var inReplyToID: String? { get } + var inReplyToAccountID: String? { get } + var content: String { get } + var createdAt: Date { get } + var reblogsCount: Int { get } + var favouritesCount: Int { get } + var reblogged: Bool { get } + var favourited: Bool { get } + var sensitive: Bool { get } + var spoilerText: String { get } + var visibility: Pachyderm.Status.Visibility { get } + var applicationName: String? { get } + var pinned: Bool? { get } + var bookmarked: Bool? { get } + + var account: Account { get } + var reblog: Status? { get } + var attachments: [Attachment] { get } + var emojis: [Emoji] { get } + var hashtags: [Hashtag] { get } + var mentions: [Mention] { get } +} diff --git a/Pachyderm/Model/Status.swift b/Pachyderm/Model/Status.swift index 9c422733..e7e62884 100644 --- a/Pachyderm/Model/Status.swift +++ b/Pachyderm/Model/Status.swift @@ -8,7 +8,7 @@ import Foundation -public class Status: Decodable { +public final class Status: StatusProtocol, Decodable { public let id: String public let uri: String public let url: URL? @@ -23,8 +23,8 @@ public class Status: Decodable { // public let repliesCount: Int public let reblogsCount: Int public let favouritesCount: Int - public let reblogged: Bool? - public let favourited: Bool? + public let reblogged: Bool + public let favourited: Bool public let muted: Bool? public let sensitive: Bool public let spoilerText: String @@ -38,6 +38,8 @@ public class Status: Decodable { public let bookmarked: Bool? public let card: Card? + public var applicationName: String? { application?.name } + public static func getContext(_ statusID: String) -> Request { return Request(method: .get, path: "/api/v1/statuses/\(statusID)/context") } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 4f42aa7c..26978680 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; }; D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; }; + D60E2F3124424F1A005F8713 /* StatusProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F3024424F1A005F8713 /* StatusProtocol.swift */; }; + D60E2F3324425374005F8713 /* AccountProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F3224425374005F8713 /* AccountProtocol.swift */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; }; D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -316,6 +318,8 @@ D60E2F252442372B005F8713 /* AccountMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMO.swift; sourceTree = ""; }; D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazilyDecoding.swift; sourceTree = ""; }; D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = ""; }; + D60E2F3024424F1A005F8713 /* StatusProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProtocol.swift; sourceTree = ""; }; + D60E2F3224425374005F8713 /* AccountProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProtocol.swift; sourceTree = ""; }; D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = ""; }; D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -591,6 +595,15 @@ path = "Attachment Gallery"; sourceTree = ""; }; + D60E2F2F24424F0D005F8713 /* Protocols */ = { + isa = PBXGroup; + children = ( + D60E2F3024424F1A005F8713 /* StatusProtocol.swift */, + D60E2F3224425374005F8713 /* AccountProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; D61099AC2144B0CC00432DC2 /* Pachyderm */ = { isa = PBXGroup; children = ( @@ -650,6 +663,7 @@ D61099DD2144C10C00432DC2 /* Model */ = { isa = PBXGroup; children = ( + D60E2F2F24424F0D005F8713 /* Protocols */, D61099DE2144C11400432DC2 /* MastodonError.swift */, D6109A04214572BF00432DC2 /* Scope.swift */, D61099E02144C1DC00432DC2 /* Account.swift */, @@ -1581,7 +1595,9 @@ D61099CB2144B20500432DC2 /* Request.swift in Sources */, D6109A05214572BF00432DC2 /* Scope.swift in Sources */, D6109A11214607D500432DC2 /* Timeline.swift in Sources */, + D60E2F3324425374005F8713 /* AccountProtocol.swift in Sources */, D61099E7214561FF00432DC2 /* Attachment.swift in Sources */, + D60E2F3124424F1A005F8713 /* StatusProtocol.swift in Sources */, D61099D02144B2D700432DC2 /* Method.swift in Sources */, D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */, D61099FB214569F600432DC2 /* Report.swift in Sources */, diff --git a/Tusker/CoreData/AccountMO.swift b/Tusker/CoreData/AccountMO.swift index db852131..ff41f5cc 100644 --- a/Tusker/CoreData/AccountMO.swift +++ b/Tusker/CoreData/AccountMO.swift @@ -12,7 +12,7 @@ import CoreData import Pachyderm @objc(AccountMO) -public final class AccountMO: NSManagedObject { +public final class AccountMO: NSManagedObject, AccountProtocol { @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "Account") @@ -20,7 +20,7 @@ public final class AccountMO: NSManagedObject { @NSManaged public var acct: String @NSManaged public var avatar: URL - @NSManaged public var bot: Bool + @NSManaged public var botCD: Bool @NSManaged public var createdAt: Date @NSManaged public var displayName: String @NSManaged private var emojisData: Data? @@ -30,7 +30,7 @@ public final class AccountMO: NSManagedObject { @NSManaged public var header: URL @NSManaged public var id: String @NSManaged public var locked: Bool - @NSManaged public var moved: Bool + @NSManaged public var movedCD: Bool @NSManaged public var note: String @NSManaged public var statusesCount: Int @NSManaged public var url: URL @@ -38,10 +38,13 @@ public final class AccountMO: NSManagedObject { @NSManaged public var movedTo: AccountMO? @LazilyDecoding(arrayFrom: \AccountMO.emojisData) - var emojis: [Emoji] + public var emojis: [Emoji] @LazilyDecoding(arrayFrom: \AccountMO.fieldsData) - var fields: [Account.Field] + public var fields: [Pachyderm.Account.Field] + + public var bot: Bool? { botCD } + public var moved: Bool? { movedCD } } @@ -59,17 +62,17 @@ extension AccountMO { self.acct = account.acct self.avatar = account.avatarStatic // we don't animate avatars - self.bot = account.bot ?? false + self.botCD = account.bot ?? false self.createdAt = account.createdAt self.displayName = account.displayName self.emojis = account.emojis - self.fields = account.fields ?? [] + self.fields = account.fields self.followersCount = account.followersCount self.followingCount = account.followingCount self.header = account.headerStatic // we don't animate headers self.id = account.id self.locked = account.locked - self.moved = account.moved ?? false + self.movedCD = account.moved ?? false self.note = account.note self.statusesCount = account.statusesCount self.url = account.url diff --git a/Tusker/CoreData/StatusMO.swift b/Tusker/CoreData/StatusMO.swift index f413ee78..827c24db 100644 --- a/Tusker/CoreData/StatusMO.swift +++ b/Tusker/CoreData/StatusMO.swift @@ -12,27 +12,27 @@ import CoreData import Pachyderm @objc(StatusMO) -public final class StatusMO: NSManagedObject { +public final class StatusMO: NSManagedObject, StatusProtocol { @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "Status") } - @NSManaged public var application: String? + @NSManaged public var applicationName: String? @NSManaged private var attachmentsData: Data? - @NSManaged public var bookmarked: Bool + @NSManaged private var bookmarkedInternal: Bool @NSManaged public var content: String @NSManaged public var createdAt: Date - @NSManaged public var emojisData: Data? + @NSManaged private var emojisData: Data? @NSManaged public var favourited: Bool @NSManaged public var favouritesCount: Int - @NSManaged public var hashtagsData: Data? + @NSManaged private var hashtagsData: Data? @NSManaged public var id: String @NSManaged public var inReplyToAccountID: String? @NSManaged public var inReplyToID: String? @NSManaged private var mentionsData: Data? @NSManaged public var muted: Bool - @NSManaged public var pinned: Bool + @NSManaged private var pinnedInternal: Bool @NSManaged public var reblogged: Bool @NSManaged public var reblogsCount: Int @NSManaged public var referenceCount: Int @@ -45,18 +45,21 @@ public final class StatusMO: NSManagedObject { @NSManaged public var reblog: StatusMO? @LazilyDecoding(arrayFrom: \StatusMO.attachmentsData) - var attachments: [Attachment] + public var attachments: [Attachment] @LazilyDecoding(arrayFrom: \StatusMO.emojisData) - var emojis: [Emoji] - + public var emojis: [Emoji] + @LazilyDecoding(arrayFrom: \StatusMO.hashtagsData) - var hashtags: [Hashtag] + public var hashtags: [Hashtag] @LazilyDecoding(arrayFrom: \StatusMO.mentionsData) - var mentions: [Mention] + public var mentions: [Mention] - var visibility: Status.Visibility { + public var pinned: Bool? { pinnedInternal } + public var bookmarked: Bool? { bookmarkedInternal } + + public var visibility: Pachyderm.Status.Visibility { get { Pachyderm.Status.Visibility(rawValue: visibilityString) ?? .public } @@ -97,13 +100,13 @@ extension StatusMO { return } - self.application = status.application?.name + self.applicationName = status.application?.name self.attachments = status.attachments - self.bookmarked = status.bookmarked ?? false + self.bookmarkedInternal = status.bookmarked ?? false self.content = status.content self.createdAt = status.createdAt self.emojis = status.emojis - self.favourited = status.favourited ?? false + self.favourited = status.favourited self.favouritesCount = status.favouritesCount self.hashtags = status.hashtags self.inReplyToAccountID = status.inReplyToAccountID @@ -111,8 +114,8 @@ extension StatusMO { self.id = status.id self.mentions = status.mentions self.muted = status.muted ?? false - self.pinned = status.pinned ?? false - self.reblogged = status.reblogged ?? false + self.pinnedInternal = status.pinned ?? false + self.reblogged = status.reblogged self.reblogsCount = status.reblogsCount self.sensitive = status.sensitive self.spoilerText = status.spoilerText diff --git a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents index c4e2508a..67a49980 100644 --- a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents +++ b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents @@ -3,7 +3,7 @@ - + @@ -13,7 +13,7 @@ - + @@ -26,13 +26,13 @@ - + - + - + @@ -40,8 +40,8 @@ - - + + @@ -49,6 +49,7 @@ + diff --git a/Tusker/LazilyDecoding.swift b/Tusker/LazilyDecoding.swift index 9858188e..c21ba79e 100644 --- a/Tusker/LazilyDecoding.swift +++ b/Tusker/LazilyDecoding.swift @@ -13,7 +13,7 @@ private let encoder = PropertyListEncoder() // todo: invalidate cache on underlying data change using KVO? @propertyWrapper -struct LazilyDecoding { +public struct LazilyDecoding { private let keyPath: ReferenceWritableKeyPath private let fallback: Value @@ -24,12 +24,12 @@ struct LazilyDecoding { self.fallback = fallback } - var wrappedValue: Value { + public var wrappedValue: Value { get { fatalError("called LazilyDecoding wrappedValue getter") } set { fatalError("called LazilyDecoding wrappedValue setter") } } - static subscript(_enclosingInstance instance: Enclosing, wrapped wrappedKeyPath: ReferenceWritableKeyPath, storage storageKeyPath: ReferenceWritableKeyPath) -> Value { + public static subscript(_enclosingInstance instance: Enclosing, wrapped wrappedKeyPath: ReferenceWritableKeyPath, storage storageKeyPath: ReferenceWritableKeyPath) -> Value { get { var wrapper = instance[keyPath: storageKeyPath] if let value = wrapper.value { diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 377b9734..7b621aea 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -207,10 +207,12 @@ extension TuskerNavigationDelegate where Self: UIViewController { guard let url = status.url else { fatalError("Missing url for status \(statusID)") } var customActivites: [UIActivity] = [OpenInSafariActivity()] - customActivites.insert(status.bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0) + let bookmarked = status.bookmarked ?? false + customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0) if status.account.id == apiController.account.id { - customActivites.insert(status.pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1) + let pinned = status.pinned ?? false + customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1) } let activityController = UIActivityViewController(activityItems: [url, status], applicationActivities: customActivites) diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 159e6db8..e68ba80f 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -261,7 +261,7 @@ class BaseStatusTableViewCell: UITableViewCell { mastodonController.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { - self.favorited = newStatus.favourited ?? false + self.favorited = newStatus.favourited self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false) UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { @@ -286,7 +286,7 @@ class BaseStatusTableViewCell: UITableViewCell { mastodonController.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { - self.reblogged = newStatus.reblogged ?? false + self.reblogged = newStatus.reblogged self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false) UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index bf7ec6ac..b182fb06 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -43,7 +43,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() } var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) - if let application = status.application { + if let application = status.applicationName { timestampAndClientText += " • \(application)" } timestampAndClientLabel.text = timestampAndClientText diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 90a5e8fc..24a4a4fb 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -80,7 +80,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { updateTimestamp() - let pinned = status.pinned + let pinned = status.pinned ?? false pinImageView.isHidden = !(pinned && showPinned) timestampLabel.isHidden = !pinImageView.isHidden }