Compare commits

..

12 Commits

42 changed files with 467 additions and 482 deletions

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class Account: Decodable { public final class Account: AccountProtocol, Decodable {
public let id: String public let id: String
public let username: String public let username: String
public let acct: String public let acct: String
@ -27,7 +27,7 @@ public class Account: Decodable {
public private(set) var emojis: [Emoji] public private(set) var emojis: [Emoji]
public let moved: Bool? public let moved: Bool?
public let movedTo: Account? public let movedTo: Account?
public let fields: [Field]? public let fields: [Field]
public let bot: Bool? public let bot: Bool?
public required init(from decoder: Decoder) throws { 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.header = try container.decode(URL.self, forKey: .header)
self.headerStatic = try container.decode(URL.self, forKey: .url) self.headerStatic = try container.decode(URL.self, forKey: .url)
self.emojis = try container.decode([Emoji].self, forKey: .emojis) 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) self.bot = try? container.decode(Bool.self, forKey: .bot)
if let moved = try? container.decode(Bool.self, forKey: .moved) { 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 import Foundation
public class Status: Decodable { public final class Status: StatusProtocol, Decodable {
public let id: String public let id: String
public let uri: String public let uri: String
public let url: URL? public let url: URL?
@ -23,8 +23,8 @@ public class Status: Decodable {
// public let repliesCount: Int // public let repliesCount: Int
public let reblogsCount: Int public let reblogsCount: Int
public let favouritesCount: Int public let favouritesCount: Int
public let reblogged: Bool? public let reblogged: Bool
public let favourited: Bool? public let favourited: Bool
public let muted: Bool? public let muted: Bool?
public let sensitive: Bool public let sensitive: Bool
public let spoilerText: String public let spoilerText: String
@ -38,6 +38,8 @@ public class Status: Decodable {
public let bookmarked: Bool? public let bookmarked: Bool?
public let card: Card? public let card: Card?
public var applicationName: String? { application?.name }
public static func getContext(_ statusID: String) -> Request<ConversationContext> { public static func getContext(_ statusID: String) -> Request<ConversationContext> {
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context") return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
} }
@ -46,14 +48,14 @@ public class Status: Decodable {
return Request<Card>(method: .get, path: "/api/v1/statuses/\(status.id)/card") return Request<Card>(method: .get, path: "/api/v1/statuses/\(status.id)/card")
} }
public static func getFavourites(_ status: Status, range: RequestRange = .default) -> Request<[Account]> { public static func getFavourites(_ statusID: String, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(status.id)/favourited_by") var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/favourited_by")
request.range = range request.range = range
return request return request
} }
public static func getReblogs(_ status: Status, range: RequestRange = .default) -> Request<[Account]> { public static func getReblogs(_ statusID: String, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(status.id)/reblogged_by") var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/reblogged_by")
request.range = range request.range = range
return request return request
} }
@ -62,20 +64,20 @@ public class Status: Decodable {
return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(status.id)") return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(status.id)")
} }
public static func reblog(_ status: Status) -> Request<Status> { public static func reblog(_ statusID: String) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/reblog") return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/reblog")
} }
public static func unreblog(_ status: Status) -> Request<Status> { public static func unreblog(_ statusID: String) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unreblog") return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unreblog")
} }
public static func favourite(_ status: Status) -> Request<Status> { public static func favourite(_ statusID: String) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/favourite") return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/favourite")
} }
public static func unfavourite(_ status: Status) -> Request<Status> { public static func unfavourite(_ statusID: String) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unfavourite") return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unfavourite")
} }
public static func pin(_ status: Status) -> Request<Status> { public static func pin(_ status: Status) -> Request<Status> {
@ -90,8 +92,8 @@ public class Status: Decodable {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/bookmark") return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/bookmark")
} }
public static func unbookmark(_ status: Status) -> Request<Status> { public static func unbookmark(_ statusID: String) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unbookmark") return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unbookmark")
} }
public static func muteConversation(_ status: Status) -> Request<Status> { public static func muteConversation(_ status: Status) -> Request<Status> {

View File

@ -9,14 +9,14 @@
import Foundation import Foundation
public class NotificationGroup { public class NotificationGroup {
public let notificationIDs: [String] public let notifications: [Notification]
public let id: String public let id: String
public let kind: Notification.Kind public let kind: Notification.Kind
public let statusState: StatusState? public let statusState: StatusState?
init?(notifications: [Notification]) { init?(notifications: [Notification]) {
guard !notifications.isEmpty else { return nil } guard !notifications.isEmpty else { return nil }
self.notificationIDs = notifications.map { $0.id } self.notifications = notifications
self.id = notifications.first!.id self.id = notifications.first!.id
self.kind = notifications.first!.kind self.kind = notifications.first!.kind
if kind == .mention { if kind == .mention {

View File

@ -20,13 +20,14 @@
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; }; D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; };
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; }; D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; }; D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; }; D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.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 */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; }; 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, ); }; }; D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -131,6 +132,7 @@
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC19123C271D9000D0238 /* MastodonActivity.swift */; }; D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC19123C271D9000D0238 /* MastodonActivity.swift */; };
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* CachedDictionary.swift */; };
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; }; D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
@ -308,7 +310,6 @@
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsViewController.swift; sourceTree = "<group>"; }; D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsViewController.swift; sourceTree = "<group>"; };
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; }; D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; }; D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
@ -316,6 +317,8 @@
D60E2F252442372B005F8713 /* AccountMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMO.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -420,6 +423,7 @@
D64BC19123C271D9000D0238 /* MastodonActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonActivity.swift; sourceTree = "<group>"; }; D64BC19123C271D9000D0238 /* MastodonActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonActivity.swift; sourceTree = "<group>"; };
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; }; D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedDictionary.swift; sourceTree = "<group>"; };
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; }; D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -591,6 +595,15 @@
path = "Attachment Gallery"; path = "Attachment Gallery";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D60E2F2F24424F0D005F8713 /* Protocols */ = {
isa = PBXGroup;
children = (
D60E2F3024424F1A005F8713 /* StatusProtocol.swift */,
D60E2F3224425374005F8713 /* AccountProtocol.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
D61099AC2144B0CC00432DC2 /* Pachyderm */ = { D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -650,6 +663,7 @@
D61099DD2144C10C00432DC2 /* Model */ = { D61099DD2144C10C00432DC2 /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D60E2F2F24424F0D005F8713 /* Protocols */,
D61099DE2144C11400432DC2 /* MastodonError.swift */, D61099DE2144C11400432DC2 /* MastodonError.swift */,
D6109A04214572BF00432DC2 /* Scope.swift */, D6109A04214572BF00432DC2 /* Scope.swift */,
D61099E02144C1DC00432DC2 /* Account.swift */, D61099E02144C1DC00432DC2 /* Account.swift */,
@ -1234,9 +1248,9 @@
D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D627FF75217E923E00CC0648 /* DraftsManager.swift */, D627FF75217E923E00CC0648 /* DraftsManager.swift */,
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */, D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */, D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6757A7A2157E00100721E32 /* XCallbackURL */, D6757A7A2157E00100721E32 /* XCallbackURL */,
@ -1581,7 +1595,9 @@
D61099CB2144B20500432DC2 /* Request.swift in Sources */, D61099CB2144B20500432DC2 /* Request.swift in Sources */,
D6109A05214572BF00432DC2 /* Scope.swift in Sources */, D6109A05214572BF00432DC2 /* Scope.swift in Sources */,
D6109A11214607D500432DC2 /* Timeline.swift in Sources */, D6109A11214607D500432DC2 /* Timeline.swift in Sources */,
D60E2F3324425374005F8713 /* AccountProtocol.swift in Sources */,
D61099E7214561FF00432DC2 /* Attachment.swift in Sources */, D61099E7214561FF00432DC2 /* Attachment.swift in Sources */,
D60E2F3124424F1A005F8713 /* StatusProtocol.swift in Sources */,
D61099D02144B2D700432DC2 /* Method.swift in Sources */, D61099D02144B2D700432DC2 /* Method.swift in Sources */,
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */, D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */,
D61099FB214569F600432DC2 /* Report.swift in Sources */, D61099FB214569F600432DC2 /* Report.swift in Sources */,
@ -1658,7 +1674,6 @@
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */, D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */, D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
@ -1751,6 +1766,7 @@
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */, D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */,
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */, D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */, D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */, 0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,

View File

@ -92,6 +92,12 @@
ReferencedContainer = "container:Tusker.xcodeproj"> ReferencedContainer = "container:Tusker.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -29,9 +29,7 @@ class FollowAccountActivity: AccountActivity {
let request = Account.follow(account.id) let request = Account.follow(account.id)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(relationship, _) = response { if case .failure(_) = response {
self.mastodonController.cache.add(relationship: relationship)
} else {
// todo: display error message // todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)
fatalError() fatalError()

View File

@ -29,9 +29,7 @@ class UnfollowAccountActivity: AccountActivity {
let request = Account.unfollow(account.id) let request = Account.unfollow(account.id)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(relationship, _) = response { if case .failure(_) = response {
self.mastodonController.cache.add(relationship: relationship)
} else {
// todo: display error message // todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)
fatalError() fatalError()

View File

@ -29,7 +29,7 @@ class BookmarkStatusActivity: StatusActivity {
let request = Status.bookmark(status) let request = Status.bookmark(status)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(status, _) = response { if case let .success(status, _) = response {
self.mastodonController.cache.add(status: status) self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
} else { } else {
// todo: display error message // todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)

View File

@ -28,7 +28,7 @@ class PinStatusActivity: StatusActivity {
let request = Status.pin(status) let request = Status.pin(status)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(status, _) = response { if case let .success(status, _) = response {
self.mastodonController.cache.add(status: status) self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
} else { } else {
// todo: display error message // todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)

View File

@ -26,10 +26,10 @@ class UnbookmarkStatusActivity: StatusActivity {
override func perform() { override func perform() {
guard let status = status else { return } guard let status = status else { return }
let request = Status.unbookmark(status) let request = Status.unbookmark(status.id)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(status, _) = response { if case let .success(status, _) = response {
self.mastodonController.cache.add(status: status) self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
} else { } else {
// todo: display error message // todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)

View File

@ -28,7 +28,7 @@ class UnpinStatusActivity: StatusActivity {
let request = Status.unpin(status) let request = Status.unpin(status)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(status, _) = response { if case let .success(status, _) = response {
self.mastodonController.cache.add(status: status) self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
} else { } else {
// todo: display error message // todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)

View File

@ -0,0 +1,35 @@
//
// CachedDictionary.swift
// Tusker
//
// Created by Shadowfacts on 5/6/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import Foundation
class CachedDictionary<Value> {
private let name: String
private var dict = [String: Value]()
private let queue: DispatchQueue
init(name: String) {
self.name = name
self.queue = DispatchQueue(label: "CachedDictionary (\(name)) Coordinator", attributes: .concurrent)
}
subscript(key: String) -> Value? {
get {
var result: Value? = nil
queue.sync {
result = dict[key]
}
return result
}
set(value) {
queue.async(flags: .barrier) {
self.dict[key] = value
}
}
}
}

View File

@ -30,8 +30,6 @@ class MastodonController {
} }
} }
private(set) lazy var cache = MastodonCache(mastodonController: self)
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self) private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self)
let instanceURL: URL let instanceURL: URL
@ -84,7 +82,7 @@ class MastodonController {
run(request) { response in run(request) { response in
guard case let .success(account, _) = response else { fatalError() } guard case let .success(account, _) = response else { fatalError() }
self.account = account self.account = account
self.cache.add(account: account) self.persistentContainer.addOrUpdate(account: account)
completion?(account) completion?(account)
} }
} }

View File

@ -12,7 +12,7 @@ import CoreData
import Pachyderm import Pachyderm
@objc(AccountMO) @objc(AccountMO)
public final class AccountMO: NSManagedObject { public final class AccountMO: NSManagedObject, AccountProtocol {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountMO> { @nonobjc public class func fetchRequest() -> NSFetchRequest<AccountMO> {
return NSFetchRequest<AccountMO>(entityName: "Account") return NSFetchRequest<AccountMO>(entityName: "Account")
@ -20,7 +20,7 @@ public final class AccountMO: NSManagedObject {
@NSManaged public var acct: String @NSManaged public var acct: String
@NSManaged public var avatar: URL @NSManaged public var avatar: URL
@NSManaged public var bot: Bool @NSManaged public var botCD: Bool
@NSManaged public var createdAt: Date @NSManaged public var createdAt: Date
@NSManaged public var displayName: String @NSManaged public var displayName: String
@NSManaged private var emojisData: Data? @NSManaged private var emojisData: Data?
@ -30,7 +30,7 @@ public final class AccountMO: NSManagedObject {
@NSManaged public var header: URL @NSManaged public var header: URL
@NSManaged public var id: String @NSManaged public var id: String
@NSManaged public var locked: Bool @NSManaged public var locked: Bool
@NSManaged public var moved: Bool @NSManaged public var movedCD: Bool
@NSManaged public var note: String @NSManaged public var note: String
@NSManaged public var statusesCount: Int @NSManaged public var statusesCount: Int
@NSManaged public var url: URL @NSManaged public var url: URL
@ -38,10 +38,13 @@ public final class AccountMO: NSManagedObject {
@NSManaged public var movedTo: AccountMO? @NSManaged public var movedTo: AccountMO?
@LazilyDecoding(arrayFrom: \AccountMO.emojisData) @LazilyDecoding(arrayFrom: \AccountMO.emojisData)
var emojis: [Emoji] public var emojis: [Emoji]
@LazilyDecoding(arrayFrom: \AccountMO.fieldsData) @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.acct = account.acct
self.avatar = account.avatarStatic // we don't animate avatars self.avatar = account.avatarStatic // we don't animate avatars
self.bot = account.bot ?? false self.botCD = account.bot ?? false
self.createdAt = account.createdAt self.createdAt = account.createdAt
self.displayName = account.displayName self.displayName = account.displayName
self.emojis = account.emojis self.emojis = account.emojis
self.fields = account.fields ?? [] self.fields = account.fields
self.followersCount = account.followersCount self.followersCount = account.followersCount
self.followingCount = account.followingCount self.followingCount = account.followingCount
self.header = account.headerStatic // we don't animate headers self.header = account.headerStatic // we don't animate headers
self.id = account.id self.id = account.id
self.locked = account.locked self.locked = account.locked
self.moved = account.moved ?? false self.movedCD = account.moved ?? false
self.note = account.note self.note = account.note
self.statusesCount = account.statusesCount self.statusesCount = account.statusesCount
self.url = account.url self.url = account.url

View File

@ -9,11 +9,15 @@
import Foundation import Foundation
import CoreData import CoreData
import Pachyderm import Pachyderm
import Combine
class MastodonCachePersistentStore: NSPersistentContainer { class MastodonCachePersistentStore: NSPersistentContainer {
private(set) lazy var backgroundContext = newBackgroundContext() private(set) lazy var backgroundContext = newBackgroundContext()
let statusSubject = PassthroughSubject<String, Never>()
let accountSubject = PassthroughSubject<String, Never>()
init(for controller: MastodonController) { init(for controller: MastodonController) {
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")! let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
let model = NSManagedObjectModel(contentsOf: url)! let model = NSManagedObjectModel(contentsOf: url)!
@ -37,27 +41,31 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
private func upsert(status: Status, incrementReferenceCount: Bool) { @discardableResult
private func upsert(status: Status, incrementReferenceCount: Bool) -> StatusMO {
if let statusMO = self.status(for: status.id, in: self.backgroundContext) { if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
statusMO.updateFrom(apiStatus: status, container: self) statusMO.updateFrom(apiStatus: status, container: self)
if incrementReferenceCount { if incrementReferenceCount {
statusMO.incrementReferenceCount() statusMO.incrementReferenceCount()
} }
return statusMO
} else { } else {
let statusMO = StatusMO(apiStatus: status, container: self, context: self.backgroundContext) let statusMO = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
if incrementReferenceCount { if incrementReferenceCount {
statusMO.incrementReferenceCount() statusMO.incrementReferenceCount()
} }
return statusMO
} }
} }
func addOrUpdate(status: Status, incrementReferenceCount: Bool, completion: (() -> Void)?) { func addOrUpdate(status: Status, incrementReferenceCount: Bool, completion: ((StatusMO) -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
self.upsert(status: status, incrementReferenceCount: incrementReferenceCount) let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount)
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
completion?() completion?(statusMO)
self.statusSubject.send(status.id)
} }
} }
@ -68,6 +76,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
completion?() completion?()
statuses.forEach { self.statusSubject.send($0.id) }
} }
} }
@ -83,31 +92,50 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
private func upsert(account: Account) { @discardableResult
private func upsert(account: Account) -> AccountMO {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) { if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self) accountMO.updateFrom(apiAccount: account, container: self)
return accountMO
} else { } else {
_ = AccountMO(apiAccount: account, container: self, context: self.backgroundContext) return AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
} }
} }
func addOrUpdate(account: Account, completion: (() -> Void)?) { func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
self.upsert(account: account) let accountMO = self.upsert(account: account)
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
completion?() completion?(accountMO)
self.accountSubject.send(account.id)
} }
} }
func addAll(accounts: [Account], completion: (() -> Void)? = nil) { func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
accounts.forEach(self.upsert(account:)) accounts.forEach { self.upsert(account: $0) }
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
completion?() completion?()
accounts.forEach { self.accountSubject.send($0.id) }
}
}
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
backgroundContext.perform {
let statuses = notifications.compactMap { $0.status }
let accounts = notifications.map { $0.account }
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
accounts.forEach { self.upsert(account: $0) }
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
completion?()
statuses.forEach { self.statusSubject.send($0.id) }
accounts.forEach { self.accountSubject.send($0.id) }
} }
} }

View File

@ -12,27 +12,27 @@ import CoreData
import Pachyderm import Pachyderm
@objc(StatusMO) @objc(StatusMO)
public final class StatusMO: NSManagedObject { public final class StatusMO: NSManagedObject, StatusProtocol {
@nonobjc public class func fetchRequest() -> NSFetchRequest<StatusMO> { @nonobjc public class func fetchRequest() -> NSFetchRequest<StatusMO> {
return NSFetchRequest<StatusMO>(entityName: "Status") return NSFetchRequest<StatusMO>(entityName: "Status")
} }
@NSManaged public var application: String? @NSManaged public var applicationName: String?
@NSManaged private var attachmentsData: Data? @NSManaged private var attachmentsData: Data?
@NSManaged public var bookmarked: Bool @NSManaged private var bookmarkedInternal: Bool
@NSManaged public var content: String @NSManaged public var content: String
@NSManaged public var createdAt: Date @NSManaged public var createdAt: Date
@NSManaged public var emojisData: Data? @NSManaged private var emojisData: Data?
@NSManaged public var favourited: Bool @NSManaged public var favourited: Bool
@NSManaged public var favouritesCount: Int @NSManaged public var favouritesCount: Int
@NSManaged public var hashtagsData: Data? @NSManaged private var hashtagsData: Data?
@NSManaged public var id: String @NSManaged public var id: String
@NSManaged public var inReplyToAccountID: String? @NSManaged public var inReplyToAccountID: String?
@NSManaged public var inReplyToID: String? @NSManaged public var inReplyToID: String?
@NSManaged private var mentionsData: Data? @NSManaged private var mentionsData: Data?
@NSManaged public var muted: Bool @NSManaged public var muted: Bool
@NSManaged public var pinned: Bool @NSManaged private var pinnedInternal: Bool
@NSManaged public var reblogged: Bool @NSManaged public var reblogged: Bool
@NSManaged public var reblogsCount: Int @NSManaged public var reblogsCount: Int
@NSManaged public var referenceCount: Int @NSManaged public var referenceCount: Int
@ -45,18 +45,21 @@ public final class StatusMO: NSManagedObject {
@NSManaged public var reblog: StatusMO? @NSManaged public var reblog: StatusMO?
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData) @LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
var attachments: [Attachment] public var attachments: [Attachment]
@LazilyDecoding(arrayFrom: \StatusMO.emojisData) @LazilyDecoding(arrayFrom: \StatusMO.emojisData)
var emojis: [Emoji] public var emojis: [Emoji]
@LazilyDecoding(arrayFrom: \StatusMO.hashtagsData) @LazilyDecoding(arrayFrom: \StatusMO.hashtagsData)
var hashtags: [Hashtag] public var hashtags: [Hashtag]
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData) @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 { get {
Pachyderm.Status.Visibility(rawValue: visibilityString) ?? .public Pachyderm.Status.Visibility(rawValue: visibilityString) ?? .public
} }
@ -97,13 +100,13 @@ extension StatusMO {
return return
} }
self.application = status.application?.name self.applicationName = status.application?.name
self.attachments = status.attachments self.attachments = status.attachments
self.bookmarked = status.bookmarked ?? false self.bookmarkedInternal = status.bookmarked ?? false
self.content = status.content self.content = status.content
self.createdAt = status.createdAt self.createdAt = status.createdAt
self.emojis = status.emojis self.emojis = status.emojis
self.favourited = status.favourited ?? false self.favourited = status.favourited
self.favouritesCount = status.favouritesCount self.favouritesCount = status.favouritesCount
self.hashtags = status.hashtags self.hashtags = status.hashtags
self.inReplyToAccountID = status.inReplyToAccountID self.inReplyToAccountID = status.inReplyToAccountID
@ -111,8 +114,8 @@ extension StatusMO {
self.id = status.id self.id = status.id
self.mentions = status.mentions self.mentions = status.mentions
self.muted = status.muted ?? false self.muted = status.muted ?? false
self.pinned = status.pinned ?? false self.pinnedInternal = status.pinned ?? false
self.reblogged = status.reblogged ?? false self.reblogged = status.reblogged
self.reblogsCount = status.reblogsCount self.reblogsCount = status.reblogsCount
self.sensitive = status.sensitive self.sensitive = status.sensitive
self.spoilerText = status.spoilerText self.spoilerText = status.spoilerText

View File

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

View File

@ -13,7 +13,7 @@ private let encoder = PropertyListEncoder()
// todo: invalidate cache on underlying data change using KVO? // todo: invalidate cache on underlying data change using KVO?
@propertyWrapper @propertyWrapper
struct LazilyDecoding<Enclosing, Value: Codable> { public struct LazilyDecoding<Enclosing, Value: Codable> {
private let keyPath: ReferenceWritableKeyPath<Enclosing, Data?> private let keyPath: ReferenceWritableKeyPath<Enclosing, Data?>
private let fallback: Value private let fallback: Value
@ -24,12 +24,12 @@ struct LazilyDecoding<Enclosing, Value: Codable> {
self.fallback = fallback self.fallback = fallback
} }
var wrappedValue: Value { public var wrappedValue: Value {
get { fatalError("called LazilyDecoding wrappedValue getter") } get { fatalError("called LazilyDecoding wrappedValue getter") }
set { fatalError("called LazilyDecoding wrappedValue setter") } 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 { get {
var wrapper = instance[keyPath: storageKeyPath] var wrapper = instance[keyPath: storageKeyPath]
if let value = wrapper.value { if let value = wrapper.value {

View File

@ -1,177 +0,0 @@
//
// StatusCache.swift
// Tusker
//
// Created by Shadowfacts on 9/17/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
import Combine
import Pachyderm
class MastodonCache {
private var statuses = CachedDictionary<Status>(name: "Statuses")
private var accounts = CachedDictionary<Account>(name: "Accounts")
private var relationships = CachedDictionary<Relationship>(name: "Relationships")
private var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications")
let statusSubject = PassthroughSubject<Status, Never>()
let accountSubject = PassthroughSubject<Account, Never>()
weak var mastodonController: MastodonController?
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
}
// MARK: - Statuses
func status(for id: String) -> Status? {
return statuses[id]
}
func set(status: Status, for id: String) {
statuses[id] = status
add(account: status.account)
if let reblog = status.reblog {
add(status: reblog)
add(account: reblog.account)
}
statusSubject.send(status)
}
func status(for id: String, completion: @escaping (Status?) -> Void) {
guard let mastodonController = mastodonController else {
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getStatus(id: id)
mastodonController.run(request) { response in
guard case let .success(status, _) = response else {
completion(nil)
return
}
self.set(status: status, for: id)
completion(status)
}
}
func add(status: Status) {
set(status: status, for: status.id)
}
func addAll(statuses: [Status]) {
statuses.forEach(add)
}
// MARK: - Accounts
func account(for id: String) -> Account? {
return accounts[id]
}
func set(account: Account, for id: String) {
accounts[id] = account
accountSubject.send(account)
}
func account(for id: String, completion: @escaping (Account?) -> Void) {
guard let mastodonController = mastodonController else {
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getAccount(id: id)
mastodonController.run(request) { response in
guard case let .success(account, _) = response else {
completion(nil)
return
}
self.set(account: account, for: account.id)
completion(account)
}
}
func add(account: Account) {
set(account: account, for: account.id)
}
func addAll(accounts: [Account]) {
accounts.forEach(add)
}
// MARK: - Relationships
func relationship(for id: String) -> Relationship? {
return relationships[id]
}
func set(relationship: Relationship, id: String) {
relationships[id] = relationship
}
func relationship(for id: String, completion: @escaping (Relationship?) -> Void) {
guard let mastodonController = mastodonController else {
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getRelationships(accounts: [id])
mastodonController.run(request) { response in
guard case let .success(relationships, _) = response,
let relationship = relationships.first else {
completion(nil)
return
}
self.set(relationship: relationship, id: relationship.id)
completion(relationship)
}
}
func add(relationship: Relationship) {
set(relationship: relationship, id: relationship.id)
}
func addAll(relationships: [Relationship]) {
relationships.forEach(add)
}
// MARK: - Notifications
func notification(for id: String) -> Pachyderm.Notification? {
return notifications[id]
}
func set(notification: Pachyderm.Notification, id: String) {
notifications[id] = notification
}
func add(notification: Pachyderm.Notification) {
set(notification: notification, id: notification.id)
}
func addAll(notifications: [Pachyderm.Notification]) {
notifications.forEach(add)
}
}
class CachedDictionary<Value> {
private let name: String
private var dict = [String: Value]()
private let queue: DispatchQueue
init(name: String) {
self.name = name
self.queue = DispatchQueue(label: "CachedDictionary (\(name)) Coordinator", attributes: .concurrent)
}
subscript(key: String) -> Value? {
get {
var result: Value? = nil
queue.sync {
result = dict[key]
}
return result
}
set(value) {
queue.async(flags: .barrier) {
self.dict[key] = value
}
}
}
}

View File

@ -45,7 +45,7 @@ class BookmarksTableViewController: EnhancedTableViewController {
let request = Client.getBookmarks() let request = Client.getBookmarks()
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
self.mastodonController.cache.addAll(statuses: statuses) self.mastodonController.persistentContainer.addAll(statuses: statuses)
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
@ -87,7 +87,7 @@ class BookmarksTableViewController: EnhancedTableViewController {
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.older = pagination?.older self.older = pagination?.older
self.mastodonController.cache.addAll(statuses: newStatuses) self.mastodonController.persistentContainer.addAll(statuses: newStatuses)
let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map { let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map {
IndexPath(row: $0, section: 0) IndexPath(row: $0, section: 0)
} }
@ -112,15 +112,15 @@ class BookmarksTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else {
return cellConfig return cellConfig
} }
let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in
let request = Status.unbookmark(status) let request = Status.unbookmark(status.id)
self.mastodonController.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(newStatus, _) = response else { fatalError() } guard case let .success(newStatus, _) = response else { fatalError() }
self.mastodonController.cache.add(status: newStatus) self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
self.statuses.remove(at: indexPath.row) self.statuses.remove(at: indexPath.row)
} }
} }
@ -138,13 +138,13 @@ class BookmarksTableViewController: EnhancedTableViewController {
} }
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] { override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { return [] } guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { return [] }
return [ return [
UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
let request = Status.unbookmark(status) let request = Status.unbookmark(status.id)
self.mastodonController.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(newStatus, _) = response else { fatalError() } guard case let .success(newStatus, _) = response else { fatalError() }
self.mastodonController.cache.add(status: newStatus) self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
self.statuses.remove(at: indexPath.row) self.statuses.remove(at: indexPath.row)
} }
}) })
@ -165,7 +165,7 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate {
extension BookmarksTableViewController: UITableViewDataSourcePrefetching { extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
_ = ImageCache.avatars.get(status.account.avatar, completion: nil) _ = ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments where attachment.kind == .image { for attachment in status.attachments where attachment.kind == .image {
_ = ImageCache.attachments.get(attachment.url, completion: nil) _ = ImageCache.attachments.get(attachment.url, completion: nil)
@ -175,7 +175,7 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
ImageCache.avatars.cancelWithoutCallback(status.account.avatar) ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
for attachment in status.attachments where attachment.kind == .image { for attachment in status.attachments where attachment.kind == .image {
ImageCache.attachments.cancelWithoutCallback(attachment.url) ImageCache.attachments.cancelWithoutCallback(attachment.url)

View File

@ -72,7 +72,7 @@ class ComposeViewController: UIViewController {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.inReplyToID = inReplyToID self.inReplyToID = inReplyToID
if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.status(for: inReplyToID) { if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.persistentContainer.status(for: inReplyToID) {
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct } accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
} else { } else {
accountsToMention = [] accountsToMention = []
@ -164,20 +164,23 @@ class ComposeViewController: UIViewController {
} }
if let inReplyToID = inReplyToID { if let inReplyToID = inReplyToID {
if let status = mastodonController.cache.status(for: inReplyToID) { if let status = mastodonController.persistentContainer.status(for: inReplyToID) {
updateInReplyTo(inReplyTo: status) updateInReplyTo(inReplyTo: status)
} else { } else {
let loadingVC = LoadingViewController() let loadingVC = LoadingViewController()
embedChild(loadingVC) embedChild(loadingVC)
mastodonController.cache.status(for: inReplyToID) { (status) in let request = Client.getStatus(id: inReplyToID)
guard let status = status else { return } mastodonController.run(request) { (response) in
guard case let .success(status, _) = response else { return }
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: true) { (status) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateInReplyTo(inReplyTo: status) self.updateInReplyTo(inReplyTo: status)
loadingVC.removeViewAndController() loadingVC.removeViewAndController()
} }
} }
} }
}
} else { } else {
visibility = Preferences.shared.defaultPostVisibility visibility = Preferences.shared.defaultPostVisibility
contentWarningEnabled = false contentWarningEnabled = false
@ -186,7 +189,7 @@ class ComposeViewController: UIViewController {
} }
} }
func updateInReplyTo(inReplyTo: Status) { func updateInReplyTo(inReplyTo: StatusMO) {
visibility = inReplyTo.visibility visibility = inReplyTo.visibility
if Preferences.shared.contentWarningCopyMode == .doNotCopy { if Preferences.shared.contentWarningCopyMode == .doNotCopy {
contentWarningEnabled = false contentWarningEnabled = false
@ -470,7 +473,7 @@ class ComposeViewController: UIViewController {
self.mastodonController.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(status, _) = response else { fatalError() } guard case let .success(status, _) = response else { fatalError() }
self.postedStatus = status self.postedStatus = status
self.mastodonController.cache.add(status: status) // self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: true)
if let draft = self.currentDraft { if let draft = self.currentDraft {
DraftsManager.shared.remove(draft) DraftsManager.shared.remove(draft)
@ -481,8 +484,8 @@ class ComposeViewController: UIViewController {
self.dismiss(animated: true) self.dismiss(animated: true)
// todo: this doesn't work // todo: this doesn't work
let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController) // let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
self.show(conversationVC, sender: self) // self.show(conversationVC, sender: self)
self.xcbSession?.complete(with: .success, additionalData: [ self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString, "statusURL": status.url?.absoluteString,

View File

@ -80,7 +80,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
self.nextRange = pagination?.older self.nextRange = pagination?.older
self.mastodonController.cache.addAll(accounts: accounts) self.mastodonController.persistentContainer.addAll(accounts: accounts)
var snapshot = self.dataSource.snapshot() var snapshot = self.dataSource.snapshot()
snapshot.deleteSections([.accounts]) snapshot.deleteSections([.accounts])

View File

@ -63,18 +63,16 @@ class NotificationsTableViewController: EnhancedTableViewController {
self.groups.append(contentsOf: groups) self.groups.append(contentsOf: groups)
self.mastodonController.cache.addAll(notifications: notifications)
self.mastodonController.cache.addAll(statuses: notifications.compactMap { $0.status })
self.mastodonController.cache.addAll(accounts: notifications.map { $0.account })
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.reloadData() self.tableView.reloadData()
} }
} }
} }
}
// MARK: - Table view data source // MARK: - Table view data source
@ -92,7 +90,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
switch group.kind { switch group.kind {
case .mention: case .mention:
guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!), guard let notification = group.notifications.first,
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else {
fatalError() fatalError()
} }
@ -113,7 +111,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
return cell return cell
case .followRequest: case .followRequest:
guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!), guard let notification = group.notifications.first,
let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() } let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() }
cell.delegate = self cell.delegate = self
cell.updateUI(notification: notification) cell.updateUI(notification: notification)
@ -143,12 +141,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
} }
self.groups.append(contentsOf: groups) self.groups.append(contentsOf: groups)
self.mastodonController.cache.addAll(notifications: newNotifications)
self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
self.older = pagination?.older self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
DispatchQueue.main.async { DispatchQueue.main.async {
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .automatic) self.tableView.insertRows(at: newIndexPaths, with: .automatic)
@ -157,6 +152,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
} }
} }
} }
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true return true
@ -195,8 +191,8 @@ class NotificationsTableViewController: EnhancedTableViewController {
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) { func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
let group = DispatchGroup() let group = DispatchGroup()
groups[indexPath.row].notificationIDs groups[indexPath.row].notifications
.map(Pachyderm.Notification.dismiss(id:)) .map { Pachyderm.Notification.dismiss(id: $0.id) }
.forEach { (request) in .forEach { (request) in
group.enter() group.enter()
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
@ -221,14 +217,11 @@ class NotificationsTableViewController: EnhancedTableViewController {
self.groups.insert(contentsOf: groups, at: 0) self.groups.insert(contentsOf: groups, at: 0)
self.mastodonController.cache.addAll(notifications: newNotifications)
self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
if let newer = pagination?.newer { if let newer = pagination?.newer {
self.newer = newer self.newer = newer
} }
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
DispatchQueue.main.async { DispatchQueue.main.async {
let newIndexPaths = (0..<groups.count).map { let newIndexPaths = (0..<groups.count).map {
IndexPath(row: $0, section: 0) IndexPath(row: $0, section: 0)
@ -244,6 +237,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
} }
} }
} }
}
} }
@ -259,8 +253,8 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
extension NotificationsTableViewController: UITableViewDataSourcePrefetching { extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
for notificationID in groups[indexPath.row].notificationIDs { for notification in groups[indexPath.row].notifications {
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue } // todo: this account object could be stale
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil) _ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
} }
} }
@ -268,8 +262,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
for notificationID in groups[indexPath.row].notificationIDs { for notification in groups[indexPath.row].notifications {
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar) ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
} }
} }

View File

@ -78,8 +78,9 @@ class ProfileTableViewController: EnhancedTableViewController {
} else { } else {
loadingVC = LoadingViewController() loadingVC = LoadingViewController()
embedChild(loadingVC!) embedChild(loadingVC!)
mastodonController.cache.account(for: accountID) { (account) in let request = Client.getAccount(id: accountID)
guard let account = account else { mastodonController.run(request) { (response) in
guard case let .success(account, _) = response else {
let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert) let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
self.navigationController!.popViewController(animated: true) self.navigationController!.popViewController(animated: true)
@ -89,7 +90,7 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
return return
} }
self.mastodonController.persistentContainer.addOrUpdate(account: account) { self.mastodonController.persistentContainer.addOrUpdate(account: account) { (_) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateAccountUI() self.updateAccountUI()
self.tableView.reloadData() self.tableView.reloadData()
@ -279,11 +280,10 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
func showMoreOptions(cell: ProfileHeaderTableViewCell) { func showMoreOptions(cell: ProfileHeaderTableViewCell) {
let account = mastodonController.persistentContainer.account(for: accountID)! let account = mastodonController.persistentContainer.account(for: accountID)!
mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in let request = Client.getRelationships(accounts: [account.id])
guard let self = self else { return } mastodonController.run(request) { (response) in
var customActivities: [UIActivity] = [OpenInSafariActivity()] var customActivities: [UIActivity] = [OpenInSafariActivity()]
if let relationship = relationship { if case let .success(results, _) = response, let relationship = results.first {
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity() let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
customActivities.insert(toggleFollowActivity, at: 0) customActivities.insert(toggleFollowActivity, at: 0)
} }

View File

@ -136,7 +136,7 @@ class SearchResultsViewController: EnhancedTableViewController {
if self.onlySections.contains(.accounts) && !results.accounts.isEmpty { if self.onlySections.contains(.accounts) && !results.accounts.isEmpty {
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts) snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
self.mastodonController.cache.addAll(accounts: results.accounts) self.mastodonController.persistentContainer.addAll(accounts: results.accounts)
} }
if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty { if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty {
snapshot.appendSections([.hashtags]) snapshot.appendSections([.hashtags])
@ -145,8 +145,8 @@ class SearchResultsViewController: EnhancedTableViewController {
if self.onlySections.contains(.statuses) && !results.statuses.isEmpty { if self.onlySections.contains(.statuses) && !results.statuses.isEmpty {
snapshot.appendSections([.statuses]) snapshot.appendSections([.statuses])
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
self.mastodonController.cache.addAll(statuses: results.statuses) self.mastodonController.persistentContainer.addAll(statuses: results.statuses)
self.mastodonController.cache.addAll(accounts: results.statuses.map { $0.account }) self.mastodonController.persistentContainer.addAll(accounts: results.statuses.map { $0.account })
} }
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot)
} }

View File

@ -73,16 +73,16 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
if accountIDs == nil { if accountIDs == nil {
// account IDs haven't been set, so perform a request to load them // account IDs haven't been set, so perform a request to load them
guard let status = mastodonController.cache.status(for: statusID) else { guard let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Missing cached status \(statusID)") fatalError("Missing cached status \(statusID)")
} }
tableView.tableFooterView = UIActivityIndicatorView(style: .large) tableView.tableFooterView = UIActivityIndicatorView(style: .large)
let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status) let request = actionType == .favorite ? Status.getFavourites(status.id) : Status.getReblogs(status.id)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(accounts, _) = response else { fatalError() } guard case let .success(accounts, _) = response else { fatalError() }
self.mastodonController.cache.addAll(accounts: accounts) self.mastodonController.persistentContainer.addAll(accounts: accounts)
DispatchQueue.main.async { DispatchQueue.main.async {
self.accountIDs = accounts.map { $0.id } self.accountIDs = accounts.map { $0.id }
self.tableView.tableFooterView = nil self.tableView.tableFooterView = nil

View File

@ -30,7 +30,7 @@ extension MenuPreviewProvider {
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] { func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] {
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let account = mastodonController.cache.account(for: accountID) else { return [] } let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
return [ return [
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in
self.navigationDelegate?.compose(mentioning: account.acct) self.navigationDelegate?.compose(mentioning: account.acct)
@ -61,7 +61,7 @@ extension MenuPreviewProvider {
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] { func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] {
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) else { return [] } let status = mastodonController.persistentContainer.status(for: statusID) else { return [] }
return [ return [
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in
self.navigationDelegate?.reply(to: statusID) self.navigationDelegate?.reply(to: statusID)

View File

@ -203,16 +203,15 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
private func moreOptions(forStatus statusID: String) -> UIActivityViewController { private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
guard let status = apiController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard let status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
guard let url = status.url else { fatalError("Missing url for status \(statusID)") } guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
var customActivites: [UIActivity] = [OpenInSafariActivity()] var customActivites: [UIActivity] = [OpenInSafariActivity()]
if let bookmarked = status.bookmarked { let bookmarked = status.bookmarked ?? false
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0) customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
}
if status.account == apiController.account, if status.account.id == apiController.account.id {
let pinned = status.pinned { let pinned = status.pinned ?? false
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1) customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
} }
@ -222,7 +221,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
private func moreOptions(forAccount accountID: String) -> UIActivityViewController { private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
guard let account = apiController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
return moreOptions(forURL: account.url) return moreOptions(forURL: account.url)
} }

View File

@ -34,7 +34,7 @@ class AccountTableViewCell: UITableViewCell {
@objc func updateUIForPrefrences() { @objc func updateUIForPrefrences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
guard let account = mastodonController.cache.account(for: accountID) else { guard let account = mastodonController.persistentContainer.account(for: accountID) else {
fatalError("Missing cached account \(accountID!)") fatalError("Missing cached account \(accountID!)")
} }
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
@ -42,7 +42,7 @@ class AccountTableViewCell: UITableViewCell {
func updateUI(accountID: String) { func updateUI(accountID: String) {
self.accountID = accountID self.accountID = accountID
guard let account = mastodonController.cache.account(for: accountID) else { guard let account = mastodonController.persistentContainer.account(for: accountID) else {
fatalError("Missing cached account \(accountID)") fatalError("Missing cached account \(accountID)")
} }

View File

@ -39,7 +39,7 @@ class ComposeStatusReplyView: UIView {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
} }
func updateUI(for status: Status) { func updateUI(for status: StatusMO) {
displayNameLabel.updateForAccountDisplayName(account: status.account) displayNameLabel.updateForAccountDisplayName(account: status.account)
usernameLabel.text = "@\(status.account.acct)" usernameLabel.text = "@\(status.account.acct)"
statusContentTextView.overrideMastodonController = mastodonController statusContentTextView.overrideMastodonController = mastodonController

View File

@ -16,6 +16,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
var mastodonController: MastodonController! { delegate?.apiController } var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var actionImageView: UIImageView! @IBOutlet weak var actionImageView: UIImageView!
@IBOutlet weak var verticalStackView: UIStackView!
@IBOutlet weak var actionAvatarStackView: UIStackView! @IBOutlet weak var actionAvatarStackView: UIStackView!
@IBOutlet weak var timestampLabel: UILabel! @IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var actionLabel: UILabel! @IBOutlet weak var actionLabel: UILabel!
@ -38,7 +39,8 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } // todo: is this compactMap necessary?
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
updateActionLabel(people: people) updateActionLabel(people: people)
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
@ -52,7 +54,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
self.group = group self.group = group
guard let firstNotification = mastodonController.cache.notification(for: group.notificationIDs.first!) else { fatalError() } guard let firstNotification = group.notifications.first else { fatalError() }
let status = firstNotification.status! let status = firstNotification.status!
self.statusID = status.id self.statusID = status.id
@ -67,9 +69,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
fatalError() fatalError()
} }
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
var imageViews = [UIImageView]()
for account in people { for account in people {
let imageView = UIImageView() let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
@ -83,11 +86,18 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
} }
actionAvatarStackView.addArrangedSubview(imageView) actionAvatarStackView.addArrangedSubview(imageView)
NSLayoutConstraint.activate([ imageViews.append(imageView)
imageView.widthAnchor.constraint(equalToConstant: 30),
imageView.heightAnchor.constraint(equalToConstant: 30) // don't add more avatars if they would overflow or squeeze the timestamp label
]) let avatarViewsWidth = 30 * CGFloat(imageViews.count)
let avatarMarginsWidth = 4 * CGFloat(max(0, imageViews.count - 1))
let maxAvatarStackWidth = verticalStackView.bounds.width - timestampLabel.bounds.width - 8
let remainingWidth = maxAvatarStackWidth - avatarViewsWidth - avatarMarginsWidth
if remainingWidth < 34 {
break
} }
}
NSLayoutConstraint.activate(imageViews.map { $0.widthAnchor.constraint(equalTo: $0.heightAnchor) })
updateTimestamp() updateTimestamp()
@ -98,8 +108,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
func updateTimestamp() { func updateTimestamp() {
guard let id = group.notificationIDs.first, guard let notification = group.notifications.first else {
let notification = mastodonController.cache.notification(for: id) else {
fatalError("Missing cached notification") fatalError("Missing cached notification")
} }
@ -126,7 +135,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
} }
func updateActionLabel(people: [Account]) { func updateActionLabel(people: [AccountMO]) {
let verb: String let verb: String
switch group.kind { switch group.kind {
case .favourite: case .favourite:
@ -163,7 +172,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
extension ActionNotificationGroupTableViewCell: SelectableTableViewCell { extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
func didSelectCell() { func didSelectCell() {
guard let delegate = delegate else { return } guard let delegate = delegate else { return }
let notifications = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)) let notifications = group.notifications
let accountIDs = notifications.map { $0.account.id } let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListTableViewController.ActionType let action: StatusActionAccountListTableViewController.ActionType
switch notifications.first!.kind { switch notifications.first!.kind {
@ -184,7 +193,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { return (content: {
let notifications = self.group.notificationIDs.compactMap(self.mastodonController.cache.notification(for:)) let notifications = self.group.notifications
let accountIDs = notifications.map { $0.account.id } let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListTableViewController.ActionType let action: StatusActionAccountListTableViewController.ActionType
switch notifications.first!.kind { switch notifications.first!.kind {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -19,19 +19,19 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hld-yu-Rmi"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hld-yu-Rmi">
<rect key="frame" x="74" y="11" width="230" height="153"/> <rect key="frame" x="74" y="11" width="230" height="153"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hTQ-P4-gOO"> <stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="hTQ-P4-gOO">
<rect key="frame" x="0.0" y="0.0" width="230" height="30"/> <rect key="frame" x="0.0" y="0.0" width="230" height="30"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="b7l-YW-nQY"> <stackView opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" ambiguous="YES" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="b7l-YW-nQY">
<rect key="frame" x="0.0" y="0.0" width="205.5" height="30"/> <rect key="frame" x="0.0" y="0.0" width="189.5" height="30"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="30" id="9uh-oo-JSM"/> <constraint firstAttribute="height" constant="30" id="9uh-oo-JSM"/>
</constraints> </constraints>
</stackView> </stackView>
<view contentMode="scaleToFill" horizontalHuggingPriority="249" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5Ef-5g-b23"> <view contentMode="scaleToFill" horizontalHuggingPriority="249" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5Ef-5g-b23">
<rect key="frame" x="205.5" y="0.0" width="0.0" height="30"/> <rect key="frame" x="197.5" y="0.0" width="0.0" height="30"/>
</view> </view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JN0-Bf-3qx"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JN0-Bf-3qx">
<rect key="frame" x="205.5" y="0.0" width="24.5" height="30"/> <rect key="frame" x="205.5" y="0.0" width="24.5" height="30"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
@ -78,6 +78,7 @@
<outlet property="actionLabel" destination="fkn-Gk-ngr" id="bBG-a8-m5G"/> <outlet property="actionLabel" destination="fkn-Gk-ngr" id="bBG-a8-m5G"/>
<outlet property="statusContentLabel" destination="lc7-zZ-HrZ" id="jgT-LU-rXt"/> <outlet property="statusContentLabel" destination="lc7-zZ-HrZ" id="jgT-LU-rXt"/>
<outlet property="timestampLabel" destination="JN0-Bf-3qx" id="Jlo-f6-DAi"/> <outlet property="timestampLabel" destination="JN0-Bf-3qx" id="Jlo-f6-DAi"/>
<outlet property="verticalStackView" destination="hld-yu-Rmi" id="jvu-1u-Ok3"/>
</connections> </connections>
<point key="canvasLocation" x="-394.20289855072468" y="56.584821428571423"/> <point key="canvasLocation" x="-394.20289855072468" y="56.584821428571423"/>
</tableViewCell> </tableViewCell>

View File

@ -34,7 +34,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
updateActionLabel(people: people) updateActionLabel(people: people)
for case let imageView as UIImageView in avatarStackView.arrangedSubviews { for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
@ -45,7 +45,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
func updateUI(group: NotificationGroup) { func updateUI(group: NotificationGroup) {
self.group = group self.group = group
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
updateActionLabel(people: people) updateActionLabel(people: people)
updateTimestamp() updateTimestamp()
@ -71,8 +71,8 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
} }
func updateActionLabel(people: [Account]) { func updateActionLabel(people: [AccountMO]) {
// todo: update to use managed objects // todo: custom emoji in people display names
// todo: figure out how to localize this // todo: figure out how to localize this
let peopleStr: String let peopleStr: String
switch people.count { switch people.count {
@ -88,8 +88,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
func updateTimestamp() { func updateTimestamp() {
guard let id = group.notificationIDs.first, guard let notification = group.notifications.first else {
let notification = mastodonController.cache.notification(for: id) else {
fatalError("Missing cached notification") fatalError("Missing cached notification")
} }
@ -128,14 +127,14 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell { extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
func didSelectCell() { func didSelectCell() {
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id } let accountIDs = group.notifications.map { $0.account.id }
switch people.count { switch accountIDs.count {
case 0: case 0:
return return
case 1: case 1:
delegate?.selected(account: people.first!) delegate?.selected(account: accountIDs.first!)
default: default:
delegate?.showFollowedByList(accountIDs: people) delegate?.showFollowedByList(accountIDs: accountIDs)
} }
} }
} }
@ -145,7 +144,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil } guard let mastodonController = mastodonController else { return nil }
let accountIDs = self.group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id } let accountIDs = self.group.notifications.map { $0.account.id }
return (content: { return (content: {
if accountIDs.count == 1 { if accountIDs.count == 1 {
return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController) return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController)

View File

@ -109,8 +109,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
@IBAction func rejectButtonPressed() { @IBAction func rejectButtonPressed() {
let request = Account.rejectFollowRequest(account) let request = Account.rejectFollowRequest(account)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(relationship, _) = response else { fatalError() } guard case .success(_, _) = response else { fatalError() }
self.mastodonController.cache.add(relationship: relationship)
DispatchQueue.main.async { DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success) UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true self.actionButtonsStackView.isHidden = true
@ -126,8 +125,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
@IBAction func acceptButtonPressed() { @IBAction func acceptButtonPressed() {
let request = Account.authorizeFollowRequest(account) let request = Account.authorizeFollowRequest(account)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(relationship, _) = response else { fatalError() } guard case .success(_, _) = response else { fatalError() }
self.mastodonController.cache.add(relationship: relationship)
DispatchQueue.main.async { DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success) UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true self.actionButtonsStackView.isHidden = true

View File

@ -88,14 +88,13 @@ class ProfileHeaderTableViewCell: UITableViewCell {
noteTextView.setTextFromHtml(account.note) noteTextView.setTextFromHtml(account.note)
noteTextView.setEmojis(account.emojis) noteTextView.setEmojis(account.emojis)
if accountID != mastodonController.account.id {
// don't show relationship label for the user's own account // don't show relationship label for the user's own account
if let relationship = mastodonController.cache.relationship(for: accountID) { if accountID != mastodonController.account.id {
followsYouLabel.isHidden = !relationship.followedBy let request = Client.getRelationships(accounts: [accountID])
} else { mastodonController.run(request) { (response) in
mastodonController.cache.relationship(for: accountID) { relationship in if case let .success(results, _) = response, let relationship = results.first {
DispatchQueue.main.async { DispatchQueue.main.async {
self.followsYouLabel.isHidden = !(relationship?.followedBy ?? false) self.followsYouLabel.isHidden = !relationship.followedBy
} }
} }
} }
@ -123,12 +122,12 @@ class ProfileHeaderTableViewCell: UITableViewCell {
fieldValuesStack.addArrangedSubview(valueTextView) fieldValuesStack.addArrangedSubview(valueTextView)
} }
// if accountUpdater == nil { if accountUpdater == nil {
// accountUpdater = mastodonController.cache.accountSubject accountUpdater = mastodonController.persistentContainer.accountSubject
// .filter { [unowned self] in $0.id == self.accountID } .filter { [unowned self] in $0 == self.accountID }
// .receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
// .sink { [unowned self] in self.updateUI(for: $0.id) } .sink { [unowned self] in self.updateUI(for: $0) }
// } }
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {

View File

@ -69,11 +69,6 @@ class BaseStatusTableViewCell: UITableViewCell {
private var statusUpdater: Cancellable? private var statusUpdater: Cancellable?
private var accountUpdater: Cancellable? private var accountUpdater: Cancellable?
deinit {
statusUpdater?.cancel()
accountUpdater?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -95,20 +90,27 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
open func createObserversIfNecessary() { open func createObserversIfNecessary() {
// todo: KVO on StatusMO for this? if statusUpdater == nil {
// if statusUpdater == nil { statusUpdater = mastodonController.persistentContainer.statusSubject
// statusUpdater = mastodonController.cache.statusSubject .filter { [unowned self] in $0 == self.statusID }
// .filter { [unowned self] in $0.id == self.statusID } .receive(on: DispatchQueue.main)
// .receive(on: DispatchQueue.main) .sink { [unowned self] in
// .sink { [unowned self] in self.updateStatusState(status: $0) } if let status = self.mastodonController.persistentContainer.status(for: $0) {
// } self.updateStatusState(status: status)
// }
// if accountUpdater == nil { }
// accountUpdater = mastodonController.cache.accountSubject }
// .filter { [unowned self] in $0.id == self.accountID }
// .receive(on: DispatchQueue.main) if accountUpdater == nil {
// .sink { [unowned self] in self.updateUI(account: $0) } accountUpdater = mastodonController.persistentContainer.accountSubject
// } .filter { [unowned self] in $0 == self.accountID }
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
if let account = self.mastodonController.persistentContainer.account(for: $0) {
self.updateUI(account: account)
}
}
}
} }
func updateUI(statusID: String, state: StatusState) { func updateUI(statusID: String, state: StatusState) {
@ -249,18 +251,18 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
@IBAction func favoritePressed() { @IBAction func favoritePressed() {
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let oldValue = favorited let oldValue = favorited
favorited = !favorited favorited = !favorited
let realStatus: Status = status.reblog ?? status let realStatus = status.reblog ?? status
let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus) let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus.id)
mastodonController.run(request) { response in mastodonController.run(request) { response in
DispatchQueue.main.async { DispatchQueue.main.async {
if case let .success(newStatus, _) = response { if case let .success(newStatus, _) = response {
self.favorited = newStatus.favourited ?? false self.favorited = newStatus.favourited
self.mastodonController.cache.add(status: newStatus) self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else { } else {
self.favorited = oldValue self.favorited = oldValue
@ -274,18 +276,18 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
@IBAction func reblogPressed() { @IBAction func reblogPressed() {
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let oldValue = reblogged let oldValue = reblogged
reblogged = !reblogged reblogged = !reblogged
let realStatus: Status = status.reblog ?? status let realStatus = status.reblog ?? status
let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus) let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus.id)
mastodonController.run(request) { response in mastodonController.run(request) { response in
DispatchQueue.main.async { DispatchQueue.main.async {
if case let .success(newStatus, _) = response { if case let .success(newStatus, _) = response {
self.reblogged = newStatus.reblogged ?? false self.reblogged = newStatus.reblogged
self.mastodonController.cache.add(status: newStatus) self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else { } else {
self.reblogged = oldValue self.reblogged = oldValue

View File

@ -43,7 +43,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() } guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() }
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
if let application = status.application { if let application = status.applicationName {
timestampAndClientText += "\(application)" timestampAndClientText += "\(application)"
} }
timestampAndClientLabel.text = timestampAndClientText timestampAndClientLabel.text = timestampAndClientText

View File

@ -47,13 +47,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
override func createObserversIfNecessary() { override func createObserversIfNecessary() {
super.createObserversIfNecessary() super.createObserversIfNecessary()
// todo: use KVO on reblogger account? if rebloggerAccountUpdater == nil {
// if rebloggerAccountUpdater == nil { rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
// rebloggerAccountUpdater = mastodonController.cache.accountSubject .filter { [unowned self] in $0 == self.rebloggerID }
// .filter { [unowned self] in $0.id == self.rebloggerID } .receive(on: DispatchQueue.main)
// .receive(on: DispatchQueue.main) .sink { [unowned self] in
// .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) } if let reblogger = self.mastodonController.persistentContainer.account(for: $0) {
// } self.updateRebloggerLabel(reblogger: reblogger)
}
}
}
} }
override func updateUI(statusID: String, state: StatusState) { override func updateUI(statusID: String, state: StatusState) {
@ -77,7 +80,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateTimestamp() updateTimestamp()
let pinned = status.pinned let pinned = status.pinned ?? false
pinImageView.isHidden = !(pinned && showPinned) pinImageView.isHidden = !(pinned && showPinned)
timestampLabel.isHidden = !pinImageView.isHidden timestampLabel.isHidden = !pinImageView.isHidden
} }
@ -133,7 +136,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
func reply() { func reply() {
if Preferences.shared.mentionReblogger, if Preferences.shared.mentionReblogger,
let rebloggerID = rebloggerID, let rebloggerID = rebloggerID,
let rebloggerAccount = mastodonController.cache.account(for: rebloggerID) { let rebloggerAccount = mastodonController.persistentContainer.account(for: rebloggerID) {
delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct) delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct)
} else { } else {
delegate?.reply(to: statusID) delegate?.reply(to: statusID)
@ -176,19 +179,19 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
guard let mastodonController = mastodonController else { return nil } guard let mastodonController = mastodonController else { return nil }
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let favoriteTitle: String let favoriteTitle: String
let favoriteRequest: Request<Status> let favoriteRequest: Request<Status>
let favoriteColor: UIColor let favoriteColor: UIColor
if status.favourited ?? false { if status.favourited {
favoriteTitle = "Unfavorite" favoriteTitle = "Unfavorite"
favoriteRequest = Status.unfavourite(status) favoriteRequest = Status.unfavourite(status.id)
favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
} else { } else {
favoriteTitle = "Favorite" favoriteTitle = "Favorite"
favoriteRequest = Status.favourite(status) favoriteRequest = Status.favourite(status.id)
favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1)
} }
let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in
@ -199,7 +202,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
return return
} }
completion(true) completion(true)
mastodonController.cache.add(status: status) mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
} }
}) })
} }
@ -209,13 +212,13 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
let reblogTitle: String let reblogTitle: String
let reblogRequest: Request<Status> let reblogRequest: Request<Status>
let reblogColor: UIColor let reblogColor: UIColor
if status.reblogged ?? false { if status.reblogged {
reblogTitle = "Unreblog" reblogTitle = "Unreblog"
reblogRequest = Status.unreblog(status) reblogRequest = Status.unreblog(status.id)
reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
} else { } else {
reblogTitle = "Reblog" reblogTitle = "Reblog"
reblogRequest = Status.reblog(status) reblogRequest = Status.reblog(status.id)
reblogColor = tintColor reblogColor = tintColor
} }
let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in
@ -226,7 +229,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
return return
} }
completion(true) completion(true)
mastodonController.cache.add(status: status) mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
} }
}) })
} }

View File

@ -27,7 +27,7 @@ class StatusContentTextView: ContentTextView {
let mention: Mention? let mention: Mention?
if let statusID = statusID, if let statusID = statusID,
let mastodonController = mastodonController, let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) { let status = mastodonController.persistentContainer.status(for: statusID) {
mention = status.mentions.first { (mention) in mention = status.mentions.first { (mention) in
// Mastodon and Pleroma include the @ in the <a> text, GNU Social does not // Mastodon and Pleroma include the @ in the <a> text, GNU Social does not
(text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host (text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host
@ -42,7 +42,7 @@ class StatusContentTextView: ContentTextView {
let hashtag: Hashtag? let hashtag: Hashtag?
if let statusID = statusID, if let statusID = statusID,
let mastodonController = mastodonController, let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) { let status = mastodonController.persistentContainer.status(for: statusID) {
hashtag = status.hashtags.first { (hashtag) in hashtag = status.hashtags.first { (hashtag) in
hashtag.url == url hashtag.url == url
} }

View File

@ -38,21 +38,21 @@ struct XCBActions {
private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) { private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) {
if let id = request.arguments["statusID"] { if let id = request.arguments["statusID"] {
mastodonController.cache.status(for: id) { (status) in let request = Client.getStatus(id: id)
if let status = status { mastodonController.run(request) { (response) in
completion(status) guard case let .success(status, _) = response else {
} else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
"error": "Could not get status with ID \(id)" "error": "Could not get status with ID \(id)"
]) ])
return
} }
completion(status)
} }
} else if let searchQuery = request.arguments["statusURL"] { } else if let searchQuery = request.arguments["statusURL"] {
let request = Client.search(query: searchQuery) let request = Client.search(query: searchQuery)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(results, _) = response, if case let .success(results, _) = response,
let status = results.statuses.first { let status = results.statuses.first {
mastodonController.cache.add(status: status)
completion(status) completion(status)
} else { } else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
@ -69,21 +69,21 @@ struct XCBActions {
private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) { private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) {
if let id = request.arguments["accountID"] { if let id = request.arguments["accountID"] {
mastodonController.cache.account(for: id) { (account) in let request = Client.getAccount(id: id)
if let account = account { mastodonController.run(request) { (response) in
completion(account) guard case let .success(account, _) = response else {
} else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
"error": "Could not get account with ID \(id)" "error": "Could not get account with ID \(id)"
]) ])
return
} }
completion(account)
} }
} else if let searchQuery = request.arguments["accountURL"] { } else if let searchQuery = request.arguments["accountURL"] {
let request = Client.search(query: searchQuery) let request = Client.search(query: searchQuery)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(results, _) = response { if case let .success(results, _) = response {
if let account = results.accounts.first { if let account = results.accounts.first {
mastodonController.cache.add(account: account)
completion(account) completion(account)
} else { } else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
@ -101,7 +101,6 @@ struct XCBActions {
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(accounts, _) = response { if case let .success(accounts, _) = response {
if let account = accounts.first { if let account = accounts.first {
mastodonController.cache.add(account: account)
completion(account) completion(account)
} else { } else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
@ -204,11 +203,10 @@ struct XCBActions {
statusAction(request: Status.reblog, alertTitle: "Reblog status?", request, session, silent) statusAction(request: Status.reblog, alertTitle: "Reblog status?", request, session, silent)
} }
static func statusAction(request: @escaping (Status) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) { static func statusAction(request: @escaping (String) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
func performAction(status: Status, completion: ((Status) -> Void)?) { func performAction(status: Status, completion: ((Status) -> Void)?) {
mastodonController.run(request(status)) { (response) in mastodonController.run(request(status.id)) { (response) in
if case let .success(status, _) = response { if case let .success(status, _) = response {
mastodonController.cache.add(status: status)
completion?(status) completion?(status)
session.complete(with: .success, additionalData: [ session.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString, "statusURL": status.url?.absoluteString,
@ -293,8 +291,7 @@ struct XCBActions {
func performAction(_ account: Account) { func performAction(_ account: Account) {
let request = Account.follow(account.id) let request = Account.follow(account.id)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(relationship, _) = response { if case .success(_, _) = response {
mastodonController.cache.add(relationship: relationship)
session.complete(with: .success, additionalData: [ session.complete(with: .success, additionalData: [
"url": account.url.absoluteString "url": account.url.absoluteString
]) ])