Add AccountProtocol and StatusProtocol

Provides a single interfaces for API and CoreData statuses and accounts
This commit is contained in:
Shadowfacts 2020-05-06 18:40:12 -04:00
parent d6c506488b
commit 90bc9b91de
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
13 changed files with 145 additions and 47 deletions

View File

@ -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) {

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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<ConversationContext> {
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
}

View File

@ -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 = "<group>"; };
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazilyDecoding.swift; sourceTree = "<group>"; };
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = "<group>"; };
D60E2F3024424F1A005F8713 /* StatusProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProtocol.swift; sourceTree = "<group>"; };
D60E2F3224425374005F8713 /* AccountProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProtocol.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -591,6 +595,15 @@
path = "Attachment Gallery";
sourceTree = "<group>";
};
D60E2F2F24424F0D005F8713 /* Protocols */ = {
isa = PBXGroup;
children = (
D60E2F3024424F1A005F8713 /* StatusProtocol.swift */,
D60E2F3224425374005F8713 /* AccountProtocol.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
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 */,

View File

@ -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<AccountMO> {
return NSFetchRequest<AccountMO>(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

View File

@ -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<StatusMO> {
return NSFetchRequest<StatusMO>(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

View File

@ -3,7 +3,7 @@
<entity name="Account" representedClassName="AccountMO" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="URI"/>
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="botCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="displayName" attributeType="String"/>
<attribute name="emojisData" attributeType="Binary"/>
@ -13,7 +13,7 @@
<attribute name="header" attributeType="URI"/>
<attribute name="id" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="moved" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="note" attributeType="String"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" attributeType="URI"/>
@ -26,13 +26,13 @@
</uniquenessConstraints>
</entity>
<entity name="Status" representedClassName="StatusMO" syncable="YES">
<attribute name="application" optional="YES" attributeType="String"/>
<attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="attachmentsData" attributeType="Binary"/>
<attribute name="bookmarked" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="emojisData" attributeType="Binary" customClassName="[Data]"/>
<attribute name="favourited" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="favourited" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hashtagsData" attributeType="Binary"/>
<attribute name="id" attributeType="String"/>
@ -40,8 +40,8 @@
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="mentionsData" attributeType="Binary"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pinned" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogged" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
@ -49,6 +49,7 @@
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibilityString" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<uniquenessConstraints>

View File

@ -13,7 +13,7 @@ private let encoder = PropertyListEncoder()
// todo: invalidate cache on underlying data change using KVO?
@propertyWrapper
struct LazilyDecoding<Enclosing, Value: Codable> {
public struct LazilyDecoding<Enclosing, Value: Codable> {
private let keyPath: ReferenceWritableKeyPath<Enclosing, Data?>
private let fallback: Value
@ -24,12 +24,12 @@ struct LazilyDecoding<Enclosing, Value: Codable> {
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<Enclosing, Value>, storage storageKeyPath: ReferenceWritableKeyPath<Enclosing, Self>) -> Value {
public static subscript(_enclosingInstance instance: Enclosing, wrapped wrappedKeyPath: ReferenceWritableKeyPath<Enclosing, Value>, storage storageKeyPath: ReferenceWritableKeyPath<Enclosing, Self>) -> Value {
get {
var wrapper = instance[keyPath: storageKeyPath]
if let value = wrapper.value {

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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
}