Compare commits
No commits in common. "4fdafa893e52dface54395bc4b921fe7511bf388" and "7fb92c9ce3939701a40d922d1250d3abfb1a337c" have entirely different histories.
4fdafa893e
...
7fb92c9ce3
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public final class Account: AccountProtocol, Decodable {
|
||||
public class Account: Decodable {
|
||||
public let id: String
|
||||
public let username: String
|
||||
public let acct: String
|
||||
|
@ -27,7 +27,7 @@ public final class Account: AccountProtocol, Decodable {
|
|||
public private(set) var emojis: [Emoji]
|
||||
public let moved: Bool?
|
||||
public let movedTo: Account?
|
||||
public let fields: [Field]
|
||||
public let fields: [Field]?
|
||||
public let bot: Bool?
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
|
@ -49,7 +49,7 @@ public final class Account: AccountProtocol, Decodable {
|
|||
self.header = try container.decode(URL.self, forKey: .header)
|
||||
self.headerStatic = try container.decode(URL.self, forKey: .url)
|
||||
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||
self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? []
|
||||
self.fields = try? container.decode([Field].self, forKey: .fields)
|
||||
self.bot = try? container.decode(Bool.self, forKey: .bot)
|
||||
|
||||
if let moved = try? container.decode(Bool.self, forKey: .moved) {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class Attachment: Codable {
|
||||
public class Attachment: Decodable {
|
||||
public let id: String
|
||||
public let kind: Kind
|
||||
public let url: URL
|
||||
|
@ -29,10 +29,18 @@ public class Attachment: Codable {
|
|||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try container.decode(String.self, forKey: .id)
|
||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||
self.url = try container.decode(URL.self, forKey: .url)
|
||||
self.previewURL = try container.decode(URL.self, forKey: .previewURL)
|
||||
self.remoteURL = try? container.decode(URL.self, forKey: .remoteURL)
|
||||
self.textURL = try? container.decode(URL.self, forKey: .textURL)
|
||||
self.url = URL(string: try container.decode(String.self, forKey: .url))!
|
||||
self.previewURL = URL(string: try container.decode(String.self, forKey: .previewURL))!
|
||||
if let remote = try? container.decode(String.self, forKey: .remoteURL) {
|
||||
self.remoteURL = URL(string: remote)!
|
||||
} else {
|
||||
self.remoteURL = nil
|
||||
}
|
||||
if let text = try? container.decode(String.self, forKey: .textURL) {
|
||||
self.textURL = URL(string: text)!
|
||||
} else {
|
||||
self.textURL = nil
|
||||
}
|
||||
self.meta = try? container.decode(Metadata.self, forKey: .meta)
|
||||
self.description = try? container.decode(String.self, forKey: .description)
|
||||
}
|
||||
|
@ -50,7 +58,7 @@ public class Attachment: Codable {
|
|||
}
|
||||
|
||||
extension Attachment {
|
||||
public enum Kind: String, Codable {
|
||||
public enum Kind: String, Decodable {
|
||||
case image
|
||||
case video
|
||||
case gifv
|
||||
|
@ -60,7 +68,7 @@ extension Attachment {
|
|||
}
|
||||
|
||||
extension Attachment {
|
||||
public class Metadata: Codable {
|
||||
public class Metadata: Decodable {
|
||||
public let length: String?
|
||||
public let duration: Float?
|
||||
public let audioEncoding: String?
|
||||
|
@ -91,7 +99,7 @@ extension Attachment {
|
|||
}
|
||||
}
|
||||
|
||||
public class ImageMetadata: Codable {
|
||||
public class ImageMetadata: Decodable {
|
||||
public let width: Int?
|
||||
public let height: Int?
|
||||
public let size: String?
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class Emoji: Codable {
|
||||
public class Emoji: Decodable {
|
||||
public let shortcode: String
|
||||
public let url: URL
|
||||
public let staticURL: URL
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class Mention: Codable {
|
||||
public class Mention: Decodable {
|
||||
public let url: URL
|
||||
public let username: String
|
||||
public let acct: String
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// 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 }
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// 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 }
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public final class Status: StatusProtocol, Decodable {
|
||||
public class Status: Decodable {
|
||||
public let id: String
|
||||
public let uri: String
|
||||
public let url: URL?
|
||||
|
@ -23,8 +23,8 @@ public final class Status: StatusProtocol, Decodable {
|
|||
// public let repliesCount: Int
|
||||
public let reblogsCount: Int
|
||||
public let favouritesCount: Int
|
||||
public let reblogged: Bool
|
||||
public let favourited: Bool
|
||||
public let reblogged: Bool?
|
||||
public let favourited: Bool?
|
||||
public let muted: Bool?
|
||||
public let sensitive: Bool
|
||||
public let spoilerText: String
|
||||
|
@ -38,24 +38,22 @@ public final class Status: StatusProtocol, Decodable {
|
|||
public let bookmarked: Bool?
|
||||
public let card: Card?
|
||||
|
||||
public var applicationName: String? { application?.name }
|
||||
|
||||
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
||||
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
|
||||
public static func getContext(_ status: Status) -> Request<ConversationContext> {
|
||||
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context")
|
||||
}
|
||||
|
||||
public static func getCard(_ status: Status) -> Request<Card> {
|
||||
return Request<Card>(method: .get, path: "/api/v1/statuses/\(status.id)/card")
|
||||
}
|
||||
|
||||
public static func getFavourites(_ statusID: String, range: RequestRange = .default) -> Request<[Account]> {
|
||||
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/favourited_by")
|
||||
public static func getFavourites(_ status: Status, range: RequestRange = .default) -> Request<[Account]> {
|
||||
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(status.id)/favourited_by")
|
||||
request.range = range
|
||||
return request
|
||||
}
|
||||
|
||||
public static func getReblogs(_ statusID: String, range: RequestRange = .default) -> Request<[Account]> {
|
||||
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/reblogged_by")
|
||||
public static func getReblogs(_ status: Status, range: RequestRange = .default) -> Request<[Account]> {
|
||||
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(status.id)/reblogged_by")
|
||||
request.range = range
|
||||
return request
|
||||
}
|
||||
|
@ -64,20 +62,20 @@ public final class Status: StatusProtocol, Decodable {
|
|||
return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(status.id)")
|
||||
}
|
||||
|
||||
public static func reblog(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/reblog")
|
||||
public static func reblog(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/reblog")
|
||||
}
|
||||
|
||||
public static func unreblog(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unreblog")
|
||||
public static func unreblog(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unreblog")
|
||||
}
|
||||
|
||||
public static func favourite(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/favourite")
|
||||
public static func favourite(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/favourite")
|
||||
}
|
||||
|
||||
public static func unfavourite(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unfavourite")
|
||||
public static func unfavourite(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unfavourite")
|
||||
}
|
||||
|
||||
public static func pin(_ status: Status) -> Request<Status> {
|
||||
|
@ -92,8 +90,8 @@ public final class Status: StatusProtocol, Decodable {
|
|||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/bookmark")
|
||||
}
|
||||
|
||||
public static func unbookmark(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unbookmark")
|
||||
public static func unbookmark(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unbookmark")
|
||||
}
|
||||
|
||||
public static func muteConversation(_ status: Status) -> Request<Status> {
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
import Foundation
|
||||
|
||||
public class NotificationGroup {
|
||||
public let notifications: [Notification]
|
||||
public let notificationIDs: [String]
|
||||
public let id: String
|
||||
public let kind: Notification.Kind
|
||||
public let statusState: StatusState?
|
||||
|
||||
|
||||
init?(notifications: [Notification]) {
|
||||
guard !notifications.isEmpty else { return nil }
|
||||
self.notifications = notifications
|
||||
self.notificationIDs = notifications.map { $0.id }
|
||||
self.id = notifications.first!.id
|
||||
self.kind = notifications.first!.kind
|
||||
if kind == .mention {
|
||||
|
|
|
@ -20,14 +20,9 @@
|
|||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.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 */; };
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
|
||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; };
|
||||
D60E2F3124424F1A005F8713 /* StatusProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F3024424F1A005F8713 /* StatusProtocol.swift */; };
|
||||
D60E2F3324425374005F8713 /* AccountProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F3224425374005F8713 /* AccountProtocol.swift */; };
|
||||
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
|
||||
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; };
|
||||
D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -110,7 +105,6 @@
|
|||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
||||
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */; };
|
||||
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */; };
|
||||
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */; };
|
||||
|
@ -132,7 +126,6 @@
|
|||
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC19123C271D9000D0238 /* MastodonActivity.swift */; };
|
||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.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 */; };
|
||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
|
||||
|
@ -157,7 +150,6 @@
|
|||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; };
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; };
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||
|
@ -165,7 +157,6 @@
|
|||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||
|
@ -312,15 +303,10 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.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>"; };
|
||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = "<group>"; };
|
||||
D60E2F3024424F1A005F8713 /* StatusProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProtocol.swift; sourceTree = "<group>"; };
|
||||
D60E2F3224425374005F8713 /* AccountProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProtocol.swift; sourceTree = "<group>"; };
|
||||
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; };
|
||||
D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -403,7 +389,6 @@
|
|||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
||||
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
||||
D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddAttachmentTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -425,7 +410,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -453,7 +437,6 @@
|
|||
D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; };
|
||||
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; };
|
||||
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
|
||||
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||
|
@ -461,7 +444,6 @@
|
|||
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
|
||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||
|
@ -599,15 +581,6 @@
|
|||
path = "Attachment Gallery";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D60E2F2F24424F0D005F8713 /* Protocols */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D60E2F3024424F1A005F8713 /* StatusProtocol.swift */,
|
||||
D60E2F3224425374005F8713 /* AccountProtocol.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -667,7 +640,6 @@
|
|||
D61099DD2144C10C00432DC2 /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D60E2F2F24424F0D005F8713 /* Protocols */,
|
||||
D61099DE2144C11400432DC2 /* MastodonError.swift */,
|
||||
D6109A04214572BF00432DC2 /* Scope.swift */,
|
||||
D61099E02144C1DC00432DC2 /* Account.swift */,
|
||||
|
@ -817,17 +789,6 @@
|
|||
path = Shortcuts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6370B9924421FE00092A7FF /* CoreData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */,
|
||||
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
||||
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -914,7 +875,6 @@
|
|||
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
|
||||
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
|
||||
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */,
|
||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */,
|
||||
);
|
||||
path = Compose;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1030,7 +990,6 @@
|
|||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */,
|
||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
|
||||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */,
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1254,10 +1213,9 @@
|
|||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
|
||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||
|
@ -1265,7 +1223,6 @@
|
|||
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||
D667E5F62135C2ED0057A976 /* Extensions */,
|
||||
D61959D2241E846D00A37B8E /* Models */,
|
||||
D6370B9924421FE00092A7FF /* CoreData */,
|
||||
D6F953F121251A2F00CF0F2B /* Controllers */,
|
||||
D641C780213DD7C4004B4513 /* Screens */,
|
||||
D6BED1722126661300F02DA0 /* Views */,
|
||||
|
@ -1601,9 +1558,7 @@
|
|||
D61099CB2144B20500432DC2 /* Request.swift in Sources */,
|
||||
D6109A05214572BF00432DC2 /* Scope.swift in Sources */,
|
||||
D6109A11214607D500432DC2 /* Timeline.swift in Sources */,
|
||||
D60E2F3324425374005F8713 /* AccountProtocol.swift in Sources */,
|
||||
D61099E7214561FF00432DC2 /* Attachment.swift in Sources */,
|
||||
D60E2F3124424F1A005F8713 /* StatusProtocol.swift in Sources */,
|
||||
D61099D02144B2D700432DC2 /* Method.swift in Sources */,
|
||||
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */,
|
||||
D61099FB214569F600432DC2 /* Report.swift in Sources */,
|
||||
|
@ -1651,7 +1606,6 @@
|
|||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||
|
@ -1678,16 +1632,14 @@
|
|||
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
|
||||
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
|
||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
|
||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */,
|
||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
||||
|
@ -1699,7 +1651,6 @@
|
|||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
||||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||
|
@ -1764,17 +1715,14 @@
|
|||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
|
||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
|
||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||
D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */,
|
||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||
|
@ -2283,19 +2231,6 @@
|
|||
productName = SheetController;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */,
|
||||
);
|
||||
currentVersion = D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */;
|
||||
path = Tusker.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
/* End XCVersionGroup section */
|
||||
};
|
||||
rootObject = D6D4DDC4212518A000E1C4BB /* Project object */;
|
||||
}
|
||||
|
|
|
@ -92,12 +92,6 @@
|
|||
ReferencedContainer = "container:Tusker.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
|
|
@ -29,7 +29,9 @@ class FollowAccountActivity: AccountActivity {
|
|||
|
||||
let request = Account.follow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case .failure(_) = response {
|
||||
if case let .success(relationship, _) = response {
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
|
|
|
@ -29,7 +29,9 @@ class UnfollowAccountActivity: AccountActivity {
|
|||
|
||||
let request = Account.unfollow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case .failure(_) = response {
|
||||
if case let .success(relationship, _) = response {
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
|
|
|
@ -29,7 +29,7 @@ class BookmarkStatusActivity: StatusActivity {
|
|||
let request = Status.bookmark(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
|
|
|
@ -28,7 +28,7 @@ class PinStatusActivity: StatusActivity {
|
|||
let request = Status.pin(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
|
|
|
@ -26,10 +26,10 @@ class UnbookmarkStatusActivity: StatusActivity {
|
|||
override func perform() {
|
||||
guard let status = status else { return }
|
||||
|
||||
let request = Status.unbookmark(status.id)
|
||||
let request = Status.unbookmark(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
|
|
|
@ -28,7 +28,7 @@ class UnpinStatusActivity: StatusActivity {
|
|||
let request = Status.unpin(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,10 +30,10 @@ class MastodonController {
|
|||
}
|
||||
}
|
||||
|
||||
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: self)
|
||||
|
||||
private(set) lazy var cache = MastodonCache(mastodonController: self)
|
||||
|
||||
let instanceURL: URL
|
||||
var accountInfo: LocalData.UserAccountInfo?
|
||||
private(set) var accountInfo: LocalData.UserAccountInfo?
|
||||
|
||||
let client: Client!
|
||||
|
||||
|
@ -82,7 +82,7 @@ class MastodonController {
|
|||
run(request) { response in
|
||||
guard case let .success(account, _) = response else { fatalError() }
|
||||
self.account = account
|
||||
self.persistentContainer.addOrUpdate(account: account)
|
||||
self.cache.add(account: account)
|
||||
completion?(account)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
//
|
||||
// AccountMO.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
|
||||
@objc(AccountMO)
|
||||
public final class AccountMO: NSManagedObject, AccountProtocol {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountMO> {
|
||||
return NSFetchRequest<AccountMO>(entityName: "Account")
|
||||
}
|
||||
|
||||
@NSManaged public var acct: String
|
||||
@NSManaged public var avatar: URL
|
||||
@NSManaged public var botCD: Bool
|
||||
@NSManaged public var createdAt: Date
|
||||
@NSManaged public var displayName: String
|
||||
@NSManaged private var emojisData: Data?
|
||||
@NSManaged private var fieldsData: Data?
|
||||
@NSManaged public var followersCount: Int
|
||||
@NSManaged public var followingCount: Int
|
||||
@NSManaged public var header: URL
|
||||
@NSManaged public var id: String
|
||||
@NSManaged public var locked: Bool
|
||||
@NSManaged public var movedCD: Bool
|
||||
@NSManaged public var note: String
|
||||
@NSManaged public var statusesCount: Int
|
||||
@NSManaged public var url: URL
|
||||
@NSManaged public var username: String
|
||||
@NSManaged public var movedTo: AccountMO?
|
||||
|
||||
@LazilyDecoding(arrayFrom: \AccountMO.emojisData)
|
||||
public var emojis: [Emoji]
|
||||
|
||||
@LazilyDecoding(arrayFrom: \AccountMO.fieldsData)
|
||||
public var fields: [Pachyderm.Account.Field]
|
||||
|
||||
public var bot: Bool? { botCD }
|
||||
public var moved: Bool? { movedCD }
|
||||
|
||||
}
|
||||
|
||||
extension AccountMO {
|
||||
convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
|
||||
self.init(context: context)
|
||||
self.updateFrom(apiAccount: account, container: container)
|
||||
}
|
||||
|
||||
func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) {
|
||||
guard let context = managedObjectContext else {
|
||||
// we've been deleted, don't bother updating
|
||||
return
|
||||
}
|
||||
|
||||
self.acct = account.acct
|
||||
self.avatar = account.avatarStatic // we don't animate avatars
|
||||
self.botCD = account.bot ?? false
|
||||
self.createdAt = account.createdAt
|
||||
self.displayName = account.displayName
|
||||
self.emojis = account.emojis
|
||||
self.fields = account.fields
|
||||
self.followersCount = account.followersCount
|
||||
self.followingCount = account.followingCount
|
||||
self.header = account.headerStatic // we don't animate headers
|
||||
self.id = account.id
|
||||
self.locked = account.locked
|
||||
self.movedCD = account.moved ?? false
|
||||
self.note = account.note
|
||||
self.statusesCount = account.statusesCount
|
||||
self.url = account.url
|
||||
self.username = account.username
|
||||
if let movedTo = account.movedTo {
|
||||
self.movedTo = container.account(for: movedTo.id, in: context) ?? AccountMO(apiAccount: movedTo, container: container, context: context)
|
||||
} else {
|
||||
self.movedTo = nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
//
|
||||
// MastodonCachePersistentStore.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
class MastodonCachePersistentStore: NSPersistentContainer {
|
||||
|
||||
private(set) lazy var backgroundContext = newBackgroundContext()
|
||||
|
||||
let statusSubject = PassthroughSubject<String, Never>()
|
||||
let accountSubject = PassthroughSubject<String, Never>()
|
||||
|
||||
init(for controller: MastodonController) {
|
||||
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
|
||||
let model = NSManagedObjectModel(contentsOf: url)!
|
||||
super.init(name: "\(controller.accountInfo!.id)_cache", managedObjectModel: model)
|
||||
loadPersistentStores { (description, error) in
|
||||
if let error = error {
|
||||
fatalError("Unable to load persistent store: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
|
||||
let context = context ?? viewContext
|
||||
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id = %@", id)
|
||||
request.fetchLimit = 1
|
||||
if let result = try? context.fetch(request), let status = result.first {
|
||||
return status
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func upsert(status: Status, incrementReferenceCount: Bool) -> StatusMO {
|
||||
if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
|
||||
statusMO.updateFrom(apiStatus: status, container: self)
|
||||
if incrementReferenceCount {
|
||||
statusMO.incrementReferenceCount()
|
||||
}
|
||||
return statusMO
|
||||
} else {
|
||||
let statusMO = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
|
||||
if incrementReferenceCount {
|
||||
statusMO.incrementReferenceCount()
|
||||
}
|
||||
return statusMO
|
||||
}
|
||||
}
|
||||
|
||||
func addOrUpdate(status: Status, incrementReferenceCount: Bool, completion: ((StatusMO) -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount)
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
completion?(statusMO)
|
||||
self.statusSubject.send(status.id)
|
||||
}
|
||||
}
|
||||
|
||||
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
completion?()
|
||||
statuses.forEach { self.statusSubject.send($0.id) }
|
||||
}
|
||||
}
|
||||
|
||||
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
|
||||
let context = context ?? viewContext
|
||||
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id = %@", id)
|
||||
request.fetchLimit = 1
|
||||
if let result = try? context.fetch(request), let account = result.first {
|
||||
return account
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func upsert(account: Account) -> AccountMO {
|
||||
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
|
||||
accountMO.updateFrom(apiAccount: account, container: self)
|
||||
return accountMO
|
||||
} else {
|
||||
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
let accountMO = self.upsert(account: account)
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
completion?(accountMO)
|
||||
self.accountSubject.send(account.id)
|
||||
}
|
||||
}
|
||||
|
||||
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
accounts.forEach { self.upsert(account: $0) }
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
//
|
||||
// StatusMO.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
|
||||
@objc(StatusMO)
|
||||
public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<StatusMO> {
|
||||
return NSFetchRequest<StatusMO>(entityName: "Status")
|
||||
}
|
||||
|
||||
@NSManaged public var applicationName: String?
|
||||
@NSManaged private var attachmentsData: Data?
|
||||
@NSManaged private var bookmarkedInternal: Bool
|
||||
@NSManaged public var content: String
|
||||
@NSManaged public var createdAt: Date
|
||||
@NSManaged private var emojisData: Data?
|
||||
@NSManaged public var favourited: Bool
|
||||
@NSManaged public var favouritesCount: Int
|
||||
@NSManaged private var hashtagsData: Data?
|
||||
@NSManaged public var id: String
|
||||
@NSManaged public var inReplyToAccountID: String?
|
||||
@NSManaged public var inReplyToID: String?
|
||||
@NSManaged private var mentionsData: Data?
|
||||
@NSManaged public var muted: Bool
|
||||
@NSManaged private var pinnedInternal: Bool
|
||||
@NSManaged public var reblogged: Bool
|
||||
@NSManaged public var reblogsCount: Int
|
||||
@NSManaged public var referenceCount: Int
|
||||
@NSManaged public var sensitive: Bool
|
||||
@NSManaged public var spoilerText: String
|
||||
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
||||
@NSManaged public var url: URL?
|
||||
@NSManaged private var visibilityString: String
|
||||
@NSManaged public var account: AccountMO
|
||||
@NSManaged public var reblog: StatusMO?
|
||||
|
||||
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
|
||||
public var attachments: [Attachment]
|
||||
|
||||
@LazilyDecoding(arrayFrom: \StatusMO.emojisData)
|
||||
public var emojis: [Emoji]
|
||||
|
||||
@LazilyDecoding(arrayFrom: \StatusMO.hashtagsData)
|
||||
public var hashtags: [Hashtag]
|
||||
|
||||
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData)
|
||||
public var mentions: [Mention]
|
||||
|
||||
public var pinned: Bool? { pinnedInternal }
|
||||
public var bookmarked: Bool? { bookmarkedInternal }
|
||||
|
||||
public var visibility: Pachyderm.Status.Visibility {
|
||||
get {
|
||||
Pachyderm.Status.Visibility(rawValue: visibilityString) ?? .public
|
||||
}
|
||||
set {
|
||||
visibilityString = newValue.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
func incrementReferenceCount() {
|
||||
referenceCount += 1
|
||||
}
|
||||
|
||||
func decrementReferenceCount() {
|
||||
referenceCount -= 1
|
||||
if referenceCount <= 0 {
|
||||
managedObjectContext!.delete(self)
|
||||
}
|
||||
}
|
||||
|
||||
public override func prepareForDeletion() {
|
||||
super.prepareForDeletion()
|
||||
reblog?.decrementReferenceCount()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusMO {
|
||||
convenience init(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
|
||||
self.init(context: context)
|
||||
self.updateFrom(apiStatus: status, container: container)
|
||||
|
||||
reblog?.incrementReferenceCount()
|
||||
}
|
||||
|
||||
func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) {
|
||||
guard let context = managedObjectContext else {
|
||||
// we have been deleted, don't bother updating
|
||||
return
|
||||
}
|
||||
|
||||
self.applicationName = status.application?.name
|
||||
self.attachments = status.attachments
|
||||
self.bookmarkedInternal = status.bookmarked ?? false
|
||||
self.content = status.content
|
||||
self.createdAt = status.createdAt
|
||||
self.emojis = status.emojis
|
||||
self.favourited = status.favourited
|
||||
self.favouritesCount = status.favouritesCount
|
||||
self.hashtags = status.hashtags
|
||||
self.inReplyToAccountID = status.inReplyToAccountID
|
||||
self.inReplyToID = status.inReplyToID
|
||||
self.id = status.id
|
||||
self.mentions = status.mentions
|
||||
self.muted = status.muted ?? false
|
||||
self.pinnedInternal = status.pinned ?? false
|
||||
self.reblogged = status.reblogged
|
||||
self.reblogsCount = status.reblogsCount
|
||||
self.sensitive = status.sensitive
|
||||
self.spoilerText = status.spoilerText
|
||||
self.uri = status.uri
|
||||
self.url = status.url
|
||||
self.visibility = status.visibility
|
||||
self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context)
|
||||
if let reblog = status.reblog {
|
||||
self.reblog = container.status(for: reblog.id, in: context) ?? StatusMO(apiStatus: reblog, container: container, context: context)
|
||||
} else {
|
||||
self.reblog = nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="URI"/>
|
||||
<attribute name="botCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="displayName" attributeType="String"/>
|
||||
<attribute name="emojisData" attributeType="Binary"/>
|
||||
<attribute name="fieldsData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" attributeType="URI"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="note" attributeType="String"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="attachmentsData" attributeType="Binary"/>
|
||||
<attribute name="bookmarkedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="emojisData" attributeType="Binary" customClassName="[Data]"/>
|
||||
<attribute name="favourited" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hashtagsData" attributeType="Binary"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||
<attribute name="mentionsData" attributeType="Binary"/>
|
||||
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="spoilerText" attributeType="String"/>
|
||||
<attribute name="uri" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="visibilityString" attributeType="String"/>
|
||||
|
||||
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
|
||||
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="313"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="418"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
extension AccountMO {
|
||||
extension Account {
|
||||
|
||||
var displayOrUserName: String {
|
||||
if displayName.isEmpty {
|
||||
|
@ -31,7 +31,7 @@ extension AccountMO {
|
|||
|
||||
private func stripCustomEmoji(from string: String) -> String {
|
||||
let range = NSRange(location: 0, length: string.utf16.count)
|
||||
return AccountMO.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
|
||||
return Account.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// PKDrawing+Render.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/9/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import PencilKit
|
||||
|
||||
extension PKDrawing {
|
||||
|
||||
func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage {
|
||||
let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
|
||||
var drawingImage: UIImage!
|
||||
lightTraitCollection.performAsCurrent {
|
||||
drawingImage = self.image(from: rect, scale: scale)
|
||||
}
|
||||
|
||||
let imageRect = CGRect(origin: .zero, size: rect.size)
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.opaque = false
|
||||
format.scale = scale
|
||||
let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)
|
||||
return renderer.image { (context) in
|
||||
UIColor.white.setFill()
|
||||
context.fill(imageRect)
|
||||
drawingImage.draw(in: imageRect)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
//
|
||||
// LazyDecoding.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private let decoder = PropertyListDecoder()
|
||||
private let encoder = PropertyListEncoder()
|
||||
|
||||
// todo: invalidate cache on underlying data change using KVO?
|
||||
@propertyWrapper
|
||||
public struct LazilyDecoding<Enclosing, Value: Codable> {
|
||||
|
||||
private let keyPath: ReferenceWritableKeyPath<Enclosing, Data?>
|
||||
private let fallback: Value
|
||||
private var value: Value?
|
||||
|
||||
init(from keyPath: ReferenceWritableKeyPath<Enclosing, Data?>, fallback: Value) {
|
||||
self.keyPath = keyPath
|
||||
self.fallback = fallback
|
||||
}
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get { fatalError("called LazilyDecoding wrappedValue getter") }
|
||||
set { fatalError("called LazilyDecoding wrappedValue setter") }
|
||||
}
|
||||
|
||||
public static subscript(_enclosingInstance instance: Enclosing, wrapped wrappedKeyPath: ReferenceWritableKeyPath<Enclosing, Value>, storage storageKeyPath: ReferenceWritableKeyPath<Enclosing, Self>) -> Value {
|
||||
get {
|
||||
var wrapper = instance[keyPath: storageKeyPath]
|
||||
if let value = wrapper.value {
|
||||
return value
|
||||
} else {
|
||||
guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
|
||||
do {
|
||||
let value = try decoder.decode(Value.self, from: data)
|
||||
wrapper.value = value
|
||||
instance[keyPath: storageKeyPath] = wrapper
|
||||
return value
|
||||
} catch {
|
||||
return wrapper.fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
set {
|
||||
var wrapper = instance[keyPath: storageKeyPath]
|
||||
wrapper.value = newValue
|
||||
instance[keyPath: storageKeyPath] = wrapper
|
||||
let newData = try? encoder.encode(newValue)
|
||||
instance[keyPath: wrapper.keyPath] = newData
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension LazilyDecoding {
|
||||
init(arrayFrom keyPath: ReferenceWritableKeyPath<Enclosing, Data?>) {
|
||||
self.init(from: keyPath, fallback: [] as! Value)
|
||||
}
|
||||
}
|
|
@ -44,10 +44,11 @@ class LocalData: ObservableObject {
|
|||
let url = URL(string: instanceURL),
|
||||
let clientId = info["clientID"],
|
||||
let secret = info["clientSecret"],
|
||||
let username = info["username"],
|
||||
let accessToken = info["accessToken"] else {
|
||||
return nil
|
||||
}
|
||||
return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: info["username"], accessToken: accessToken)
|
||||
return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken)
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
|
@ -55,18 +56,15 @@ class LocalData: ObservableObject {
|
|||
}
|
||||
set {
|
||||
objectWillChange.send()
|
||||
let array = newValue.map { (info) -> [String: String] in
|
||||
var res = [
|
||||
let array = newValue.map { (info) in
|
||||
return [
|
||||
"id": info.id,
|
||||
"instanceURL": info.instanceURL.absoluteString,
|
||||
"clientID": info.clientID,
|
||||
"clientSecret": info.clientSecret,
|
||||
"username": info.username,
|
||||
"accessToken": info.accessToken
|
||||
]
|
||||
if let username = info.username {
|
||||
res["username"] = username
|
||||
}
|
||||
return res
|
||||
}
|
||||
defaults.set(array, forKey: accountsKey)
|
||||
}
|
||||
|
@ -87,7 +85,7 @@ class LocalData: ObservableObject {
|
|||
return !accounts.isEmpty
|
||||
}
|
||||
|
||||
func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String) -> UserAccountInfo {
|
||||
func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo {
|
||||
var accounts = self.accounts
|
||||
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
|
||||
accounts.remove(at: index)
|
||||
|
@ -99,13 +97,6 @@ class LocalData: ObservableObject {
|
|||
return info
|
||||
}
|
||||
|
||||
func setUsername(for info: UserAccountInfo, username: String) {
|
||||
var info = info
|
||||
info.username = username
|
||||
removeAccount(info)
|
||||
accounts.append(info)
|
||||
}
|
||||
|
||||
func removeAccount(_ info: UserAccountInfo) {
|
||||
accounts.removeAll(where: { $0.id == info.id })
|
||||
}
|
||||
|
@ -137,7 +128,7 @@ extension LocalData {
|
|||
let instanceURL: URL
|
||||
let clientID: String
|
||||
let clientSecret: String
|
||||
fileprivate(set) var username: String!
|
||||
let username: String
|
||||
let accessToken: String
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,13 +9,11 @@
|
|||
import UIKit
|
||||
import Photos
|
||||
import MobileCoreServices
|
||||
import PencilKit
|
||||
|
||||
enum CompositionAttachmentData {
|
||||
case asset(PHAsset)
|
||||
case image(UIImage)
|
||||
case video(URL)
|
||||
case drawing(PKDrawing)
|
||||
|
||||
var type: AttachmentType {
|
||||
switch self {
|
||||
|
@ -25,8 +23,6 @@ enum CompositionAttachmentData {
|
|||
return .image
|
||||
case .video(_):
|
||||
return .video
|
||||
case .drawing(_):
|
||||
return .image
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,7 +44,7 @@ enum CompositionAttachmentData {
|
|||
}
|
||||
}
|
||||
|
||||
func getData(completion: @escaping (_ data: Data, _ mimeType: String) -> Void) {
|
||||
func getData(completion: @escaping (Data, String) -> Void) {
|
||||
switch self {
|
||||
case let .image(image):
|
||||
completion(image.pngData()!, "image/png")
|
||||
|
@ -94,10 +90,6 @@ enum CompositionAttachmentData {
|
|||
fatalError("failed to create export session")
|
||||
}
|
||||
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
||||
|
||||
case let .drawing(drawing):
|
||||
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
|
||||
completion(image.pngData()!, "image/png")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,10 +138,6 @@ extension CompositionAttachmentData: Codable {
|
|||
try container.encode(image.pngData()!, forKey: .imageData)
|
||||
case .video(_):
|
||||
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded"))
|
||||
case let .drawing(drawing):
|
||||
try container.encode("drawing", forKey: .type)
|
||||
let drawingData = drawing.dataRepresentation()
|
||||
try container.encode(drawingData, forKey: .drawing)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,10 +156,6 @@ extension CompositionAttachmentData: Codable {
|
|||
throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data")
|
||||
}
|
||||
self = .image(image)
|
||||
case "drawing":
|
||||
let drawingData = try container.decode(Data.self, forKey: .drawing)
|
||||
let drawing = try PKDrawing(data: drawingData)
|
||||
self = .drawing(drawing)
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'")
|
||||
}
|
||||
|
@ -182,8 +166,6 @@ extension CompositionAttachmentData: Codable {
|
|||
case imageData
|
||||
/// The local identifier of the PHAsset for this attachment
|
||||
case assetIdentifier
|
||||
/// The PKDrawing object for this attachment.
|
||||
case drawing
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,8 +178,6 @@ extension CompositionAttachmentData: Equatable {
|
|||
return a == b
|
||||
case let (.video(a), .video(b)):
|
||||
return a == b
|
||||
case let (.drawing(a), .drawing(b)):
|
||||
return a == b
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -109,8 +109,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
|
||||
try! scene.session.mastodonController?.persistentContainer.viewContext.save()
|
||||
}
|
||||
|
||||
func activateAccount(_ account: LocalData.UserAccountInfo) {
|
||||
|
|
|
@ -52,9 +52,6 @@ class AssetPreviewViewController: UIViewController {
|
|||
default:
|
||||
fatalError("asset mediaType must be image or video")
|
||||
}
|
||||
case let .drawing(drawing):
|
||||
let image = drawing.imageInLightMode(from: drawing.bounds)
|
||||
showImage(image)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
let request = Client.getBookmarks()
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses)
|
||||
self.mastodonController.cache.addAll(statuses: statuses)
|
||||
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
@ -87,7 +87,7 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
self.older = pagination?.older
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses)
|
||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||
let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
|
@ -112,15 +112,15 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else {
|
||||
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else {
|
||||
return cellConfig
|
||||
}
|
||||
|
||||
let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in
|
||||
let request = Status.unbookmark(status.id)
|
||||
let request = Status.unbookmark(status)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(newStatus, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||
self.mastodonController.cache.add(status: newStatus)
|
||||
self.statuses.remove(at: indexPath.row)
|
||||
}
|
||||
}
|
||||
|
@ -138,13 +138,13 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
|
||||
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { return [] }
|
||||
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { 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
|
||||
let request = Status.unbookmark(status.id)
|
||||
let request = Status.unbookmark(status)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(newStatus, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||
self.mastodonController.cache.add(status: newStatus)
|
||||
self.statuses.remove(at: indexPath.row)
|
||||
}
|
||||
})
|
||||
|
@ -165,7 +165,7 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
|||
extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
|
@ -175,7 +175,7 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
import MobileCoreServices
|
||||
import PencilKit
|
||||
|
||||
protocol ComposeAttachmentsViewControllerDelegate: class {
|
||||
func composeSelectedAttachmentsDidChange()
|
||||
|
@ -38,8 +37,6 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var currentlyEditedDrawingIndex: Int?
|
||||
|
||||
init(attachments: [CompositionAttachment], mastodonController: MastodonController) {
|
||||
self.attachments = attachments
|
||||
|
@ -142,23 +139,6 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func presentComposeDrawingViewController(editingAttachmentAt attachmentIndex: Int? = nil) {
|
||||
let drawingVC: ComposeDrawingViewController
|
||||
|
||||
if let index = attachmentIndex,
|
||||
case let .drawing(drawing) = attachments[index].data {
|
||||
drawingVC = ComposeDrawingViewController(editing: drawing)
|
||||
currentlyEditedDrawingIndex = index
|
||||
} else {
|
||||
drawingVC = ComposeDrawingViewController()
|
||||
}
|
||||
|
||||
drawingVC.delegate = self
|
||||
let nav = UINavigationController(rootViewController: drawingVC)
|
||||
nav.modalPresentationStyle = .fullScreen
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
func uploadAll(stepProgress: @escaping () -> Void, completion: @escaping (_ success: Bool, _ uploadedAttachments: [Attachment]) -> Void) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
|
@ -290,49 +270,19 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
if indexPath.section == 0 {
|
||||
let attachment = attachments[indexPath.row]
|
||||
// cast to NSIndexPath because identifier needs to conform to NSCopying
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
|
||||
return AssetPreviewViewController(attachment: attachment.data)
|
||||
}) { (_) -> UIMenu? in
|
||||
var actions = [UIAction]()
|
||||
|
||||
switch attachment.data {
|
||||
case .drawing(_):
|
||||
actions.append(UIAction(title: "Edit Drawing", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
self.presentComposeDrawingViewController(editingAttachmentAt: indexPath.row)
|
||||
}))
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if actions.isEmpty {
|
||||
return nil
|
||||
} else {
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||
}
|
||||
}
|
||||
} else if indexPath.section == 1 {
|
||||
guard isAddAttachmentsButtonEnabled() else {
|
||||
return nil
|
||||
}
|
||||
// show context menu for drawing/file uploads
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
||||
UIAction(title: "Draw Something", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
self.presentComposeDrawingViewController()
|
||||
})
|
||||
])
|
||||
}
|
||||
} else {
|
||||
guard indexPath.section == 0 else { return nil }
|
||||
|
||||
let attachment = attachments[indexPath.row]
|
||||
// cast to NSIndexPath because identifier needs to conform to NSCopying
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
|
||||
return AssetPreviewViewController(attachment: attachment.data)
|
||||
}) { (_) -> UIMenu? in
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func targetedPreview(forConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
|
||||
indexPath.section == 0,
|
||||
let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell {
|
||||
let parameters = UIPreviewParameters()
|
||||
parameters.backgroundColor = .black
|
||||
|
@ -502,26 +452,3 @@ extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelega
|
|||
delegate?.composeRequiresAttachmentDescriptionsDidChange()
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: ComposeDrawingViewControllerDelegate {
|
||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) {
|
||||
dismiss(animated: true)
|
||||
currentlyEditedDrawingIndex = nil
|
||||
}
|
||||
|
||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) {
|
||||
let newAttachment = CompositionAttachment(data: .drawing(drawing))
|
||||
|
||||
if let currentlyEditedDrawingIndex = currentlyEditedDrawingIndex {
|
||||
attachments[currentlyEditedDrawingIndex] = newAttachment
|
||||
tableView.reloadRows(at: [IndexPath(row: currentlyEditedDrawingIndex, section: 0)], with: .automatic)
|
||||
} else {
|
||||
attachments.append(newAttachment)
|
||||
tableView.insertRows(at: [IndexPath(row: self.attachments.count - 1, section: 0)], with: .automatic)
|
||||
updateHeightConstraint()
|
||||
}
|
||||
|
||||
dismiss(animated: true)
|
||||
currentlyEditedDrawingIndex = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
//
|
||||
// ComposeDrawingViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/7/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import PencilKit
|
||||
|
||||
protocol ComposeDrawingViewControllerDelegate: class {
|
||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
|
||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
|
||||
}
|
||||
|
||||
class ComposeDrawingViewController: UIViewController {
|
||||
|
||||
weak var delegate: ComposeDrawingViewControllerDelegate?
|
||||
|
||||
private(set) var canvasView: PKCanvasView!
|
||||
private(set) var cancelBarButtonItem: UIBarButtonItem!
|
||||
private(set) var undoBarButtonItem: UIBarButtonItem!
|
||||
private(set) var redoBarButtonItem: UIBarButtonItem!
|
||||
|
||||
private var initialDrawing: PKDrawing?
|
||||
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
convenience init(editing initialDrawing: PKDrawing) {
|
||||
self.init()
|
||||
|
||||
self.initialDrawing = initialDrawing
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
overrideUserInterfaceStyle = .light
|
||||
|
||||
navigationItem.title = NSLocalizedString("Draw", comment: "compose drawing screen title")
|
||||
cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed))
|
||||
undoBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrow.uturn.left.circle"), style: .plain, target: self, action: #selector(undoPressed))
|
||||
undoBarButtonItem.isEnabled = false
|
||||
redoBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrow.uturn.right.circle"), style: .plain, target: self, action: #selector(redoPressed))
|
||||
redoBarButtonItem.isEnabled = false
|
||||
navigationItem.leftBarButtonItems = [cancelBarButtonItem]
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(savePressed))
|
||||
|
||||
canvasView = PKCanvasView()
|
||||
if let initialDrawing = initialDrawing {
|
||||
canvasView.drawing = initialDrawing
|
||||
}
|
||||
canvasView.delegate = self
|
||||
canvasView.allowsFingerDrawing = true
|
||||
canvasView.minimumZoomScale = 0.5
|
||||
canvasView.maximumZoomScale = 2
|
||||
canvasView.backgroundColor = .systemBackground
|
||||
canvasView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(canvasView)
|
||||
NSLayoutConstraint.activate([
|
||||
canvasView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
canvasView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
canvasView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if let window = parent?.view.window, let toolPicker = PKToolPicker.shared(for: window) {
|
||||
toolPicker.setVisible(true, forFirstResponder: canvasView)
|
||||
toolPicker.addObserver(canvasView)
|
||||
toolPicker.addObserver(self)
|
||||
|
||||
updateLayout(for: toolPicker)
|
||||
canvasView.becomeFirstResponder()
|
||||
|
||||
// wait until the next run loop iteration so that the canvas view has become first responder and it's undo manager exists
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(for toolPicker: PKToolPicker) {
|
||||
let obscuredFrame = toolPicker.frameObscured(in: view)
|
||||
|
||||
// if there is no obscured frame, the tool picker is floating
|
||||
// which means we don't need to change inset or show undo/redo
|
||||
if obscuredFrame.isNull {
|
||||
navigationItem.leftBarButtonItems = [cancelBarButtonItem]
|
||||
canvasView.contentInset = .zero
|
||||
canvasView.automaticallyAdjustsScrollIndicatorInsets = true
|
||||
} else {
|
||||
navigationItem.leftBarButtonItems = [cancelBarButtonItem, undoBarButtonItem, redoBarButtonItem]
|
||||
canvasView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: obscuredFrame.height, right: 0)
|
||||
canvasView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
// we don't care about the botom safe area inset becaused the tool picker obscured frame includes the safe area
|
||||
canvasView.scrollIndicatorInsets = UIEdgeInsets(top: view.safeAreaInsets.top, left: view.safeAreaInsets.left, bottom: obscuredFrame.height, right: view.safeAreaInsets.right)
|
||||
}
|
||||
}
|
||||
|
||||
func updateContentSizeForDrawing() {
|
||||
let overscrollAmount: CGFloat = 100
|
||||
let drawingBounds = canvasView.drawing.bounds
|
||||
let newSize: CGSize
|
||||
if !drawingBounds.isNull {
|
||||
let width = max(canvasView.bounds.width, (drawingBounds.maxX + overscrollAmount) * canvasView.zoomScale)
|
||||
let height = max(canvasView.bounds.height, (drawingBounds.maxY + overscrollAmount) * canvasView.zoomScale)
|
||||
newSize = CGSize(width: width, height: height)
|
||||
} else {
|
||||
newSize = canvasView.bounds.size
|
||||
}
|
||||
canvasView.contentSize = newSize
|
||||
}
|
||||
|
||||
@objc func updateUndoRedoButtonState() {
|
||||
undoBarButtonItem.isEnabled = undoManager!.canUndo
|
||||
redoBarButtonItem.isEnabled = undoManager!.canRedo
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc func cancelPressed() {
|
||||
delegate?.composeDrawingViewControllerClose(self)
|
||||
}
|
||||
|
||||
@objc func savePressed() {
|
||||
delegate?.composeDrawingViewController(self, saveDrawing: canvasView.drawing)
|
||||
}
|
||||
|
||||
@objc func undoPressed() {
|
||||
undoManager!.undo()
|
||||
}
|
||||
|
||||
@objc func redoPressed() {
|
||||
undoManager!.redo()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeDrawingViewController: PKCanvasViewDelegate {
|
||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||
if !undoManager!.isUndoing && !undoManager!.isRedoing {
|
||||
updateUndoRedoButtonState()
|
||||
}
|
||||
updateContentSizeForDrawing()
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeDrawingViewController: PKToolPickerObserver {
|
||||
func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
|
||||
updateLayout(for: toolPicker)
|
||||
}
|
||||
|
||||
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
|
||||
updateLayout(for: toolPicker)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeDrawingViewController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return otherGestureRecognizer == canvasView.drawingGestureRecognizer
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ class ComposeViewController: UIViewController {
|
|||
self.mastodonController = mastodonController
|
||||
|
||||
self.inReplyToID = inReplyToID
|
||||
if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.persistentContainer.status(for: inReplyToID) {
|
||||
if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.status(for: inReplyToID) {
|
||||
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
|
||||
} else {
|
||||
accountsToMention = []
|
||||
|
@ -164,20 +164,17 @@ class ComposeViewController: UIViewController {
|
|||
}
|
||||
|
||||
if let inReplyToID = inReplyToID {
|
||||
if let status = mastodonController.persistentContainer.status(for: inReplyToID) {
|
||||
if let status = mastodonController.cache.status(for: inReplyToID) {
|
||||
updateInReplyTo(inReplyTo: status)
|
||||
} else {
|
||||
let loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC)
|
||||
|
||||
let request = Client.getStatus(id: inReplyToID)
|
||||
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 {
|
||||
self.updateInReplyTo(inReplyTo: status)
|
||||
loadingVC.removeViewAndController()
|
||||
}
|
||||
mastodonController.cache.status(for: inReplyToID) { (status) in
|
||||
guard let status = status else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.updateInReplyTo(inReplyTo: status)
|
||||
loadingVC.removeViewAndController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +186,7 @@ class ComposeViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func updateInReplyTo(inReplyTo: StatusMO) {
|
||||
func updateInReplyTo(inReplyTo: Status) {
|
||||
visibility = inReplyTo.visibility
|
||||
if Preferences.shared.contentWarningCopyMode == .doNotCopy {
|
||||
contentWarningEnabled = false
|
||||
|
@ -216,8 +213,7 @@ class ComposeViewController: UIViewController {
|
|||
replyAvatarImageViewTopConstraint!.isActive = true
|
||||
|
||||
inReplyToContainer.isHidden = false
|
||||
// todo: update to use managed objects
|
||||
inReplyToLabel.text = "In reply to \(inReplyTo.account.displayName)"
|
||||
inReplyToLabel.text = "In reply to \(inReplyTo.account.displayOrUserName)"
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -473,7 +469,7 @@ class ComposeViewController: UIViewController {
|
|||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(status, _) = response else { fatalError() }
|
||||
self.postedStatus = status
|
||||
// self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: true)
|
||||
self.mastodonController.cache.add(status: status)
|
||||
|
||||
if let draft = self.currentDraft {
|
||||
DraftsManager.shared.remove(draft)
|
||||
|
@ -484,8 +480,8 @@ class ComposeViewController: UIViewController {
|
|||
self.dismiss(animated: true)
|
||||
|
||||
// todo: this doesn't work
|
||||
// let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
|
||||
// self.show(conversationVC, sender: self)
|
||||
let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
|
||||
self.show(conversationVC, sender: self)
|
||||
|
||||
self.xcbSession?.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
|
|
|
@ -42,13 +42,6 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
mastodonController.persistentContainer.status(for: mainStatusID)?.decrementReferenceCount()
|
||||
for (id, _) in statuses {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -65,42 +58,32 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
|
||||
statuses = [(mainStatusID, mainStatusState)]
|
||||
|
||||
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
||||
fatalError("Missing cached status \(self.mainStatusID)")
|
||||
}
|
||||
let mainStatusInReplyToID = mainStatus.inReplyToID
|
||||
mainStatus.incrementReferenceCount()
|
||||
|
||||
let request = Status.getContext(mainStatusID)
|
||||
guard let mainStatus = mastodonController.cache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") }
|
||||
|
||||
let request = Status.getContext(mainStatus)
|
||||
mastodonController.run(request) { response in
|
||||
guard case let .success(context, _) = response else { fatalError() }
|
||||
|
||||
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses) {
|
||||
self.mastodonController.persistentContainer.addAll(statuses: context.descendants) {
|
||||
self.statuses = parents.map { ($0, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
|
||||
let indexPath = IndexPath(row: parents.count, section: 0)
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
||||
}
|
||||
}
|
||||
let parents = self.getDirectParents(of: mainStatus, from: context.ancestors)
|
||||
self.mastodonController.cache.addAll(statuses: parents)
|
||||
self.mastodonController.cache.addAll(statuses: context.descendants)
|
||||
self.statuses = parents.map { ($0.id, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
|
||||
let indexPath = IndexPath(row: parents.count, section: 0)
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
func getDirectParents(of status: Status, from statuses: [Status]) -> [Status] {
|
||||
var statuses = statuses
|
||||
var parents = [String]()
|
||||
|
||||
var parentID: String? = inReplyToID
|
||||
|
||||
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
|
||||
let parentStatus = statuses.remove(at: parentIndex)
|
||||
parents.insert(parentStatus.id, at: 0)
|
||||
parentID = parentStatus.inReplyToID
|
||||
var parents: [Status] = []
|
||||
var currentStatus: Status? = status
|
||||
while currentStatus != nil {
|
||||
guard let index = statuses.firstIndex(where: { $0.id == currentStatus!.inReplyToID }) else { break }
|
||||
let parent = statuses.remove(at: index)
|
||||
parents.insert(parent, at: 0)
|
||||
currentStatus = parent
|
||||
}
|
||||
|
||||
return parents
|
||||
}
|
||||
|
||||
|
@ -186,7 +169,7 @@ extension ConversationTableViewController: StatusTableViewCellDelegate {
|
|||
extension ConversationTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
|
@ -196,7 +179,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
|
|
|
@ -80,7 +80,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
|||
|
||||
self.nextRange = pagination?.older
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(accounts: accounts)
|
||||
self.mastodonController.cache.addAll(accounts: accounts)
|
||||
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteSections([.accounts])
|
||||
|
|
|
@ -63,13 +63,15 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
|
||||
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.older = pagination?.older
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +92,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
|
||||
switch group.kind {
|
||||
case .mention:
|
||||
guard let notification = group.notifications.first,
|
||||
guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!),
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else {
|
||||
fatalError()
|
||||
}
|
||||
|
@ -111,7 +113,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
return cell
|
||||
|
||||
case .followRequest:
|
||||
guard let notification = group.notifications.first,
|
||||
guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!),
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() }
|
||||
cell.delegate = self
|
||||
cell.updateUI(notification: notification)
|
||||
|
@ -140,14 +142,16 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
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.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,8 +195,8 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
|
||||
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
groups[indexPath.row].notifications
|
||||
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
||||
groups[indexPath.row].notificationIDs
|
||||
.map(Pachyderm.Notification.dismiss(id:))
|
||||
.forEach { (request) in
|
||||
group.enter()
|
||||
mastodonController.run(request) { (response) in
|
||||
|
@ -217,24 +221,26 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
|
||||
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 {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||
DispatchQueue.main.async {
|
||||
let newIndexPaths = (0..<groups.count).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
self.refreshControl?.endRefreshing()
|
||||
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
self.tableView.scrollToRow(at: IndexPath(row: newNotifications.count, section: 0), at: .top, animated: false)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let newIndexPaths = (0..<groups.count).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
self.refreshControl?.endRefreshing()
|
||||
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
self.tableView.scrollToRow(at: IndexPath(row: newNotifications.count, section: 0), at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -253,8 +259,8 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
|||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
for notification in groups[indexPath.row].notifications {
|
||||
// todo: this account object could be stale
|
||||
for notificationID in groups[indexPath.row].notificationIDs {
|
||||
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
|
||||
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
||||
}
|
||||
}
|
||||
|
@ -262,7 +268,8 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
for notification in groups[indexPath.row].notifications {
|
||||
for notificationID in groups[indexPath.row].notificationIDs {
|
||||
guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,13 +68,10 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
|
|||
let authCode = item.value else { return }
|
||||
|
||||
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
|
||||
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
|
||||
mastodonController.accountInfo = accountInfo
|
||||
|
||||
mastodonController.getOwnAccount { (account) in
|
||||
LocalData.shared.setUsername(for: accountInfo, username: account.username)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
|
||||
|
||||
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,14 +73,13 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
tableView.prefetchDataSource = self
|
||||
|
||||
if let accountID = accountID {
|
||||
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||
if mastodonController.cache.account(for: accountID) != nil {
|
||||
updateAccountUI()
|
||||
} else {
|
||||
loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC!)
|
||||
let request = Client.getAccount(id: accountID)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(account, _) = response else {
|
||||
mastodonController.cache.account(for: accountID) { (account) in
|
||||
guard account != nil else {
|
||||
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
|
||||
self.navigationController!.popViewController(animated: true)
|
||||
|
@ -90,11 +89,9 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
return
|
||||
}
|
||||
self.mastodonController.persistentContainer.addOrUpdate(account: account) { (_) in
|
||||
DispatchQueue.main.async {
|
||||
self.updateAccountUI()
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.updateAccountUI()
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,25 +112,23 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
getStatuses(onlyPinned: true) { (response) in
|
||||
guard case let .success(statuses, _) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||
}
|
||||
self.mastodonController.cache.addAll(statuses: statuses)
|
||||
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||
}
|
||||
|
||||
getStatuses() { response in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
self.newer = pagination?.newer
|
||||
}
|
||||
self.mastodonController.cache.addAll(statuses: statuses)
|
||||
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
self.newer = pagination?.newer
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
guard let accountID = accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
|
||||
guard let accountID = accountID, let account = mastodonController.cache.account(for: accountID) else { return }
|
||||
navigationItem.title = account.displayNameWithoutCustomEmoji
|
||||
}
|
||||
|
||||
|
@ -143,7 +138,7 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
|
||||
func sendMessageMentioning() {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController))
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
@ -157,7 +152,7 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == 0 {
|
||||
return accountID == nil || mastodonController.persistentContainer.account(for: accountID) == nil ? 0 : 1
|
||||
return accountID == nil || mastodonController.cache.account(for: accountID) == nil ? 0 : 1
|
||||
} else if section == 1 {
|
||||
return pinnedStatuses.count
|
||||
} else {
|
||||
|
@ -192,20 +187,16 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// todo: if scrolling up, remove statuses at bottom like timeline VC
|
||||
|
||||
// load older statuses if at bottom
|
||||
if timelineSegments.count > 0 && indexPath.section - 1 == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 2].count - 1 {
|
||||
guard let older = older else { return }
|
||||
|
||||
getStatuses(for: older) { response in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
}
|
||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,35 +219,34 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
getStatuses(for: newer) { response in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.refreshControl?.endRefreshing()
|
||||
}
|
||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.refreshControl?.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
getStatuses(onlyPinned: true) { (response) in
|
||||
guard case let .success(newPinnedStatuses, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatuses) {
|
||||
let oldPinnedStatuses = self.pinnedStatuses
|
||||
var pinnedStatuses = [(id: String, state: StatusState)]()
|
||||
for status in newPinnedStatuses {
|
||||
let state: StatusState
|
||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||
state = oldState
|
||||
} else {
|
||||
state = .unknown
|
||||
}
|
||||
pinnedStatuses.append((status.id, state))
|
||||
self.mastodonController.cache.addAll(statuses: newPinnedStatuses)
|
||||
|
||||
let oldPinnedStatuses = self.pinnedStatuses
|
||||
var pinnedStatuses = [(id: String, state: StatusState)]()
|
||||
for status in newPinnedStatuses {
|
||||
let state: StatusState
|
||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||
state = oldState
|
||||
} else {
|
||||
state = .unknown
|
||||
}
|
||||
self.pinnedStatuses = pinnedStatuses
|
||||
pinnedStatuses.append((status.id, state))
|
||||
}
|
||||
self.pinnedStatuses = pinnedStatuses
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,12 +268,13 @@ extension ProfileTableViewController: StatusTableViewCellDelegate {
|
|||
|
||||
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
||||
func showMoreOptions(cell: ProfileHeaderTableViewCell) {
|
||||
let account = mastodonController.persistentContainer.account(for: accountID)!
|
||||
let account = mastodonController.cache.account(for: accountID)!
|
||||
|
||||
let request = Client.getRelationships(accounts: [account.id])
|
||||
mastodonController.run(request) { (response) in
|
||||
mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in
|
||||
guard let self = self else { return }
|
||||
|
||||
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
||||
if case let .success(results, _) = response, let relationship = results.first {
|
||||
if let relationship = relationship {
|
||||
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
||||
customActivities.insert(toggleFollowActivity, at: 0)
|
||||
}
|
||||
|
@ -302,7 +293,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
|
|||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths where indexPath.section > 1 {
|
||||
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { continue }
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
|
@ -313,7 +304,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
|
|||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths where indexPath.section > 1 {
|
||||
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
|
|
|
@ -136,7 +136,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
if self.onlySections.contains(.accounts) && !results.accounts.isEmpty {
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||
self.mastodonController.persistentContainer.addAll(accounts: results.accounts)
|
||||
self.mastodonController.cache.addAll(accounts: results.accounts)
|
||||
}
|
||||
if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty {
|
||||
snapshot.appendSections([.hashtags])
|
||||
|
@ -145,8 +145,8 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
if self.onlySections.contains(.statuses) && !results.statuses.isEmpty {
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||
self.mastodonController.persistentContainer.addAll(statuses: results.statuses)
|
||||
self.mastodonController.persistentContainer.addAll(accounts: results.statuses.map { $0.account })
|
||||
self.mastodonController.cache.addAll(statuses: results.statuses)
|
||||
self.mastodonController.cache.addAll(accounts: results.statuses.map { $0.account })
|
||||
}
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
|
|
|
@ -73,16 +73,16 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
|
|||
|
||||
if accountIDs == nil {
|
||||
// account IDs haven't been set, so perform a request to load them
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
guard let status = mastodonController.cache.status(for: statusID) else {
|
||||
fatalError("Missing cached status \(statusID)")
|
||||
}
|
||||
|
||||
tableView.tableFooterView = UIActivityIndicatorView(style: .large)
|
||||
|
||||
let request = actionType == .favorite ? Status.getFavourites(status.id) : Status.getReblogs(status.id)
|
||||
let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(accounts, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(accounts: accounts)
|
||||
self.mastodonController.cache.addAll(accounts: accounts)
|
||||
DispatchQueue.main.async {
|
||||
self.accountIDs = accounts.map { $0.id }
|
||||
self.tableView.tableFooterView = nil
|
||||
|
|
|
@ -19,9 +19,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
var newer: RequestRange?
|
||||
var older: RequestRange?
|
||||
|
||||
private var prevScrollViewContentOffset: CGPoint?
|
||||
private var scrollViewDirection: CGFloat = 0
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||
self.timeline = timeline
|
||||
self.mastodonController = mastodonController
|
||||
|
@ -41,17 +38,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
// decrement reference counts of any statuses we still have
|
||||
// if the app is currently being quit, this will not affect the persisted data because
|
||||
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
||||
for segment in timelineSegments {
|
||||
for (id, _) in segment {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func statusID(for indexPath: IndexPath) -> String {
|
||||
return timelineSegments[indexPath.section][indexPath.row].id
|
||||
}
|
||||
|
@ -73,14 +59,12 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
let request = Client.getStatuses(timeline: timeline)
|
||||
mastodonController.run(request) { response in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
// todo: possible race condition here? we update the underlying data before waiting to reload the table view
|
||||
self.mastodonController.cache.addAll(statuses: statuses)
|
||||
self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0)
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,59 +93,6 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// when scrolling upwards, decrement reference counts for old statuses, if necessary
|
||||
if scrollViewDirection < 0 {
|
||||
if indexPath.section <= timelineSegments.count - 2 {
|
||||
// decrement ref counts for all sections below the section below the current section
|
||||
// (e.g., there exist sections 0, 1, 2 and we're currently scrolling upwards in section 0, we want to remove section 2)
|
||||
|
||||
// todo: this is in the hot path for scrolling, possibly move this to a background thread?
|
||||
let sectionsToRemove = indexPath.section + 1..<timelineSegments.count
|
||||
for section in sectionsToRemove {
|
||||
for (id, _) in timelineSegments.remove(at: section) {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
// see below comment about DispatchQueue.main.async
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we are scrolling in the last or second to last section
|
||||
// grab the last section and, if there is more than one page in the last section
|
||||
// (we never want to remove the only page in the last section unless there is another section between it and the user),
|
||||
// remove the last page and decrement reference counts
|
||||
let pageSize = 20 // todo: this should come from somewhere in Pachyderm
|
||||
let lastSection = timelineSegments.last!
|
||||
if lastSection.count > 2 * pageSize,
|
||||
indexPath.row < lastSection.count - (2 * pageSize) {
|
||||
// todo: this is in the hot path for scrolling, possibly move this to a background thread?
|
||||
let statusesToRemove = lastSection[lastSection.count - pageSize..<lastSection.count]
|
||||
for (id, _) in statusesToRemove {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
timelineSegments[timelineSegments.count - 1].removeLast(20)
|
||||
|
||||
let removedIndexPaths = (lastSection.count - 20..<lastSection.count).map { IndexPath(row: $0, section: timelineSegments.count - 1) }
|
||||
// Removing this DispatchQueue.main.async call causes things to break when scrolling
|
||||
// back down towards the removed rows. There would be a index out of bounds crash
|
||||
// because, while we've already removed the statuses from our model, the table view doesn't seem to know that.
|
||||
// It seems like tableView update calls made from inside tableView(_:willDisplay:forRowAt:) are silently ignored.
|
||||
// Calling tableView.numberOfRows(inSection: 0) when trapped in the debugger after the aforementioned IOOB
|
||||
// will produce an incorrect value (it will be some multiple of pageSize too high).
|
||||
// Deferring the tableView update until the next runloop iteration seems to solve that.
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteRows(at: removedIndexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load older statuses, if necessary
|
||||
if indexPath.section == timelineSegments.count - 1,
|
||||
indexPath.row == timelineSegments[indexPath.section].count - 1 {
|
||||
guard let older = older else { return }
|
||||
|
@ -170,14 +101,13 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
mastodonController.run(request) { response in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
self.older = pagination?.older
|
||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||
let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count)
|
||||
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.timelineSegments.count - 1) }
|
||||
self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,39 +133,29 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
mastodonController.run(request) { response in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
self.newer = pagination?.newer
|
||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
DispatchQueue.main.async {
|
||||
let newIndexPaths = (0..<newStatuses.count).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
self.refreshControl?.endRefreshing()
|
||||
|
||||
// maintain the current position in the list (don't scroll to the top)
|
||||
self.tableView.scrollToRow(at: IndexPath(row: newStatuses.count, section: 0), at: .top, animated: false)
|
||||
DispatchQueue.main.async {
|
||||
let newIndexPaths = (0..<newStatuses.count).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
self.refreshControl?.endRefreshing()
|
||||
|
||||
// maintain the current position in the list (don't scroll to the top)
|
||||
self.tableView.scrollToRow(at: IndexPath(row: newStatuses.count, section: 0), at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark: Scroll View Delegate
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if let prev = prevScrollViewContentOffset {
|
||||
scrollViewDirection = scrollView.contentOffset.y - prev.y
|
||||
}
|
||||
prevScrollViewContentOffset = scrollView.contentOffset
|
||||
}
|
||||
|
||||
|
||||
@objc func composePressed(_ sender: Any) {
|
||||
compose()
|
||||
}
|
||||
|
@ -255,7 +175,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
|
|||
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue }
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
|
@ -265,10 +185,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
// todo: this means when removing cells, we can't cancel prefetching
|
||||
// is this an issue?
|
||||
guard indexPath.section < timelineSegments.count, indexPath.row < timelineSegments[indexPath.section].count,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||
guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
|
|
|
@ -30,7 +30,7 @@ extension MenuPreviewProvider {
|
|||
|
||||
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] {
|
||||
guard let mastodonController = mastodonController,
|
||||
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
||||
let account = mastodonController.cache.account(for: accountID) else { return [] }
|
||||
return [
|
||||
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in
|
||||
self.navigationDelegate?.compose(mentioning: account.acct)
|
||||
|
@ -61,7 +61,7 @@ extension MenuPreviewProvider {
|
|||
|
||||
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] {
|
||||
guard let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return [] }
|
||||
let status = mastodonController.cache.status(for: statusID) else { return [] }
|
||||
return [
|
||||
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in
|
||||
self.navigationDelegate?.reply(to: statusID)
|
||||
|
|
|
@ -32,13 +32,12 @@ class UserActivityManager {
|
|||
|
||||
// MARK: - New Post
|
||||
static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity {
|
||||
// todo: update to use managed objects
|
||||
let activity = NSUserActivity(type: .newPost)
|
||||
activity.isEligibleForPrediction = true
|
||||
if let mentioning = mentioning {
|
||||
activity.userInfo = ["mentioning": mentioning.acct]
|
||||
activity.title = "Send a message to \(mentioning.displayName)"
|
||||
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)"
|
||||
activity.title = "Send a message to \(mentioning.displayOrUserName)"
|
||||
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayOrUserName)"
|
||||
} else {
|
||||
activity.userInfo = [:]
|
||||
activity.title = "New Post"
|
||||
|
|
|
@ -203,15 +203,16 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
}
|
||||
|
||||
private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
|
||||
guard let status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||
guard let status = apiController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||
guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
|
||||
var customActivites: [UIActivity] = [OpenInSafariActivity()]
|
||||
|
||||
let bookmarked = status.bookmarked ?? false
|
||||
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
|
||||
|
||||
if status.account.id == apiController.account.id {
|
||||
let pinned = status.pinned ?? false
|
||||
if let bookmarked = status.bookmarked {
|
||||
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
|
||||
}
|
||||
|
||||
if status.account == apiController.account,
|
||||
let pinned = status.pinned {
|
||||
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
|
||||
}
|
||||
|
||||
|
@ -221,7 +222,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
}
|
||||
|
||||
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
|
||||
guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||
guard let account = apiController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||
return moreOptions(forURL: account.url)
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class AccountTableViewCell: UITableViewCell {
|
|||
@objc func updateUIForPrefrences() {
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
guard let account = mastodonController.cache.account(for: accountID) else {
|
||||
fatalError("Missing cached account \(accountID!)")
|
||||
}
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
|
@ -42,7 +42,7 @@ class AccountTableViewCell: UITableViewCell {
|
|||
|
||||
func updateUI(accountID: String) {
|
||||
self.accountID = accountID
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
guard let account = mastodonController.cache.account(for: accountID) else {
|
||||
fatalError("Missing cached account \(accountID)")
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
//import Combine
|
||||
import Photos
|
||||
import AVFoundation
|
||||
|
||||
|
@ -41,8 +42,6 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
descriptionTextView.text = attachment.attachmentDescription
|
||||
updateDescriptionPlaceholderLabel()
|
||||
|
||||
assetImageView.contentMode = .scaleAspectFill
|
||||
assetImageView.backgroundColor = .secondarySystemBackground
|
||||
switch attachment.data {
|
||||
case let .image(image):
|
||||
assetImageView.image = image
|
||||
|
@ -58,10 +57,6 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
|
||||
assetImageView.image = UIImage(cgImage: cgImage)
|
||||
}
|
||||
case let .drawing(drawing):
|
||||
assetImageView.image = drawing.imageInLightMode(from: drawing.bounds)
|
||||
assetImageView.contentMode = .scaleAspectFit
|
||||
assetImageView.backgroundColor = .white
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ class AttachmentsContainerView: UIView {
|
|||
|
||||
// MARK: - User Interaface
|
||||
|
||||
func updateUI(status: StatusMO) {
|
||||
func updateUI(status: Status) {
|
||||
self.statusID = status.id
|
||||
attachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) }
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class ComposeStatusReplyView: UIView {
|
|||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
}
|
||||
|
||||
func updateUI(for status: StatusMO) {
|
||||
func updateUI(for status: Status) {
|
||||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||
usernameLabel.text = "@\(status.account.acct)"
|
||||
statusContentTextView.overrideMastodonController = mastodonController
|
||||
|
|
|
@ -171,13 +171,8 @@ class ContentTextView: LinkTextView {
|
|||
}
|
||||
}
|
||||
|
||||
// only handles link taps via the gesture recognizer which is used when selection is disabled
|
||||
@objc func textTapped(_ recognizer: UITapGestureRecognizer) {
|
||||
// if there currently is a selection, deselct it on single-tap
|
||||
if selectedRange.length > 0 {
|
||||
// location doesn't matter since we are non-editable and the cursor isn't visible
|
||||
selectedRange = NSRange(location: 0, length: 0)
|
||||
}
|
||||
|
||||
let location = recognizer.location(in: self)
|
||||
if let (link, range) = getLinkAtPoint(location) {
|
||||
let text = (self.text as NSString).substring(with: range)
|
||||
|
|
|
@ -34,9 +34,6 @@ class DraftTableViewCell: UITableViewCell {
|
|||
attachmentsStackView.addArrangedSubview(imageView)
|
||||
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
|
||||
|
||||
imageView.backgroundColor = .secondarySystemBackground
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
|
||||
switch attachment.data {
|
||||
case let .asset(asset):
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
|
||||
|
@ -47,10 +44,6 @@ class DraftTableViewCell: UITableViewCell {
|
|||
case .video(_):
|
||||
// videos aren't saved to drafts, so this is unreachable
|
||||
return
|
||||
case let .drawing(drawing):
|
||||
imageView.image = drawing.imageInLightMode(from: drawing.bounds)
|
||||
imageView.backgroundColor = .white
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,15 +83,6 @@ class EmojiLabel: UILabel {
|
|||
|
||||
extension EmojiLabel {
|
||||
func updateForAccountDisplayName(account: Account) {
|
||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||
self.text = account.displayName
|
||||
self.removeEmojis()
|
||||
} else {
|
||||
self.text = account.displayName
|
||||
self.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
}
|
||||
func updateForAccountDisplayName(account: AccountMO) {
|
||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||
self.text = account.displayNameWithoutCustomEmoji
|
||||
self.removeEmojis()
|
||||
|
|
|
@ -39,8 +39,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
// todo: is this compactMap necessary?
|
||||
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
|
||||
updateActionLabel(people: people)
|
||||
|
||||
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
|
||||
|
@ -54,7 +53,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
self.group = group
|
||||
|
||||
guard let firstNotification = group.notifications.first else { fatalError() }
|
||||
guard let firstNotification = mastodonController.cache.notification(for: group.notificationIDs.first!) else { fatalError() }
|
||||
let status = firstNotification.status!
|
||||
self.statusID = status.id
|
||||
|
||||
|
@ -69,7 +68,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
fatalError()
|
||||
}
|
||||
|
||||
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
|
||||
|
||||
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
var imageViews = [UIImageView]()
|
||||
|
@ -108,7 +107,8 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
func updateTimestamp() {
|
||||
guard let notification = group.notifications.first else {
|
||||
guard let id = group.notificationIDs.first,
|
||||
let notification = mastodonController.cache.notification(for: id) else {
|
||||
fatalError("Missing cached notification")
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
func updateActionLabel(people: [AccountMO]) {
|
||||
func updateActionLabel(people: [Account]) {
|
||||
let verb: String
|
||||
switch group.kind {
|
||||
case .favourite:
|
||||
|
@ -147,14 +147,13 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
let peopleStr: String
|
||||
// todo: figure out how to localize this
|
||||
// todo: update to use managed objects
|
||||
switch people.count {
|
||||
case 1:
|
||||
peopleStr = people.first!.displayName
|
||||
peopleStr = people.first!.displayOrUserName
|
||||
case 2:
|
||||
peopleStr = people.first!.displayName + " and " + people.last!.displayName
|
||||
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName
|
||||
default:
|
||||
peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
|
||||
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName
|
||||
}
|
||||
actionLabel.text = "\(verb) by \(peopleStr)"
|
||||
}
|
||||
|
@ -172,7 +171,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||
func didSelectCell() {
|
||||
guard let delegate = delegate else { return }
|
||||
let notifications = group.notifications
|
||||
let notifications = group.notificationIDs.compactMap(mastodonController.cache.notification(for:))
|
||||
let accountIDs = notifications.map { $0.account.id }
|
||||
let action: StatusActionAccountListTableViewController.ActionType
|
||||
switch notifications.first!.kind {
|
||||
|
@ -193,7 +192,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
|||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return (content: {
|
||||
let notifications = self.group.notifications
|
||||
let notifications = self.group.notificationIDs.compactMap(self.mastodonController.cache.notification(for:))
|
||||
let accountIDs = notifications.map { $0.account.id }
|
||||
let action: StatusActionAccountListTableViewController.ActionType
|
||||
switch notifications.first!.kind {
|
||||
|
|
|
@ -34,7 +34,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
|
||||
updateActionLabel(people: people)
|
||||
|
||||
for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
|
||||
|
@ -45,8 +45,8 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
func updateUI(group: NotificationGroup) {
|
||||
self.group = group
|
||||
|
||||
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||
|
||||
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
|
||||
|
||||
updateActionLabel(people: people)
|
||||
updateTimestamp()
|
||||
|
||||
|
@ -71,24 +71,24 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
func updateActionLabel(people: [AccountMO]) {
|
||||
// todo: custom emoji in people display names
|
||||
func updateActionLabel(people: [Account]) {
|
||||
// todo: figure out how to localize this
|
||||
let peopleStr: String
|
||||
switch people.count {
|
||||
case 1:
|
||||
peopleStr = people.first!.displayName
|
||||
peopleStr = people.first!.displayOrUserName
|
||||
case 2:
|
||||
peopleStr = people.first!.displayName + " and " + people.last!.displayName
|
||||
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName
|
||||
default:
|
||||
peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
|
||||
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName
|
||||
|
||||
}
|
||||
actionLabel.text = "Followed by \(peopleStr)"
|
||||
}
|
||||
|
||||
func updateTimestamp() {
|
||||
guard let notification = group.notifications.first else {
|
||||
guard let id = group.notificationIDs.first,
|
||||
let notification = mastodonController.cache.notification(for: id) else {
|
||||
fatalError("Missing cached notification")
|
||||
}
|
||||
|
||||
|
@ -127,14 +127,14 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
|
||||
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||
func didSelectCell() {
|
||||
let accountIDs = group.notifications.map { $0.account.id }
|
||||
switch accountIDs.count {
|
||||
let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id }
|
||||
switch people.count {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
delegate?.selected(account: accountIDs.first!)
|
||||
delegate?.selected(account: people.first!)
|
||||
default:
|
||||
delegate?.showFollowedByList(accountIDs: accountIDs)
|
||||
delegate?.showFollowedByList(accountIDs: people)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
|||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
guard let mastodonController = mastodonController else { return nil }
|
||||
let accountIDs = self.group.notifications.map { $0.account.id }
|
||||
let accountIDs = self.group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id }
|
||||
return (content: {
|
||||
if accountIDs.count == 1 {
|
||||
return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController)
|
||||
|
|
|
@ -52,13 +52,12 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
func updateUI(account: Account) {
|
||||
// todo: update to use managed objects
|
||||
self.account = account
|
||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||
actionLabel.text = "Request to follow from \(account.displayName)"
|
||||
actionLabel.text = "Request to follow from \(account.displayNameWithoutCustomEmoji)"
|
||||
actionLabel.removeEmojis()
|
||||
} else {
|
||||
actionLabel.text = "Request to follow from \(account.displayName)"
|
||||
actionLabel.text = "Request to follow from \(account.displayOrUserName)"
|
||||
actionLabel.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
||||
|
@ -109,7 +108,8 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
|||
@IBAction func rejectButtonPressed() {
|
||||
let request = Account.rejectFollowRequest(account)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case .success(_, _) = response else { fatalError() }
|
||||
guard case let .success(relationship, _) = response else { fatalError() }
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
DispatchQueue.main.async {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
self.actionButtonsStackView.isHidden = true
|
||||
|
@ -125,7 +125,8 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
|||
@IBAction func acceptButtonPressed() {
|
||||
let request = Account.authorizeFollowRequest(account)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case .success(_, _) = response else { fatalError() }
|
||||
guard case let .success(relationship, _) = response else { fatalError() }
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
DispatchQueue.main.async {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
self.actionButtonsStackView.isHidden = true
|
||||
|
|
|
@ -63,7 +63,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
guard accountID != self.accountID else { return }
|
||||
self.accountID = accountID
|
||||
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||
|
||||
updateUIForPreferences()
|
||||
|
||||
|
@ -88,50 +88,55 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
noteTextView.setTextFromHtml(account.note)
|
||||
noteTextView.setEmojis(account.emojis)
|
||||
|
||||
// don't show relationship label for the user's own account
|
||||
if accountID != mastodonController.account.id {
|
||||
let request = Client.getRelationships(accounts: [accountID])
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(results, _) = response, let relationship = results.first {
|
||||
// don't show relationship label for the user's own account
|
||||
if let relationship = mastodonController.cache.relationship(for: accountID) {
|
||||
followsYouLabel.isHidden = !relationship.followedBy
|
||||
} else {
|
||||
mastodonController.cache.relationship(for: accountID) { relationship in
|
||||
DispatchQueue.main.async {
|
||||
self.followsYouLabel.isHidden = !relationship.followedBy
|
||||
self.followsYouLabel.isHidden = !(relationship?.followedBy ?? false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldsStackView.isHidden = account.fields.isEmpty
|
||||
|
||||
fieldsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
for field in account.fields {
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = field.name
|
||||
nameLabel.font = .boldSystemFont(ofSize: 17)
|
||||
nameLabel.textAlignment = .right
|
||||
nameLabel.numberOfLines = 0
|
||||
fieldNamesStackView.addArrangedSubview(nameLabel)
|
||||
|
||||
let valueTextView = ContentTextView()
|
||||
valueTextView.isSelectable = false
|
||||
valueTextView.font = .systemFont(ofSize: 17)
|
||||
valueTextView.setTextFromHtml(field.value)
|
||||
valueTextView.setEmojis(account.emojis)
|
||||
valueTextView.textAlignment = .left
|
||||
valueTextView.awakeFromNib()
|
||||
valueTextView.navigationDelegate = delegate
|
||||
fieldValuesStack.addArrangedSubview(valueTextView)
|
||||
if let fields = account.fields, !fields.isEmpty {
|
||||
fieldsStackView.isHidden = false
|
||||
|
||||
fieldsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
for field in fields {
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = field.name
|
||||
nameLabel.font = .boldSystemFont(ofSize: 17)
|
||||
nameLabel.textAlignment = .right
|
||||
nameLabel.numberOfLines = 0
|
||||
fieldNamesStackView.addArrangedSubview(nameLabel)
|
||||
|
||||
let valueTextView = ContentTextView()
|
||||
valueTextView.isSelectable = false
|
||||
valueTextView.font = .systemFont(ofSize: 17)
|
||||
valueTextView.setTextFromHtml(field.value)
|
||||
valueTextView.setEmojis(account.emojis)
|
||||
valueTextView.textAlignment = .left
|
||||
valueTextView.awakeFromNib()
|
||||
valueTextView.navigationDelegate = delegate
|
||||
fieldValuesStack.addArrangedSubview(valueTextView)
|
||||
}
|
||||
} else {
|
||||
fieldsStackView.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
if accountUpdater == nil {
|
||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [unowned self] in $0 == self.accountID }
|
||||
accountUpdater = mastodonController.cache.accountSubject
|
||||
.filter { [unowned self] in $0.id == self.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [unowned self] in self.updateUI(for: $0) }
|
||||
.sink { [unowned self] in self.updateUI(for: $0.id) }
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
|
||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
|
@ -148,12 +153,12 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
@objc func avatarPressed() {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
|
||||
}
|
||||
|
||||
@objc func headerPressed() {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||
delegate?.showLoadingLargeImage(url: account.header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,11 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
private var statusUpdater: Cancellable?
|
||||
private var accountUpdater: Cancellable?
|
||||
|
||||
deinit {
|
||||
statusUpdater?.cancel()
|
||||
accountUpdater?.cancel()
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
@ -91,32 +96,24 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
|
||||
open func createObserversIfNecessary() {
|
||||
if statusUpdater == nil {
|
||||
statusUpdater = mastodonController.persistentContainer.statusSubject
|
||||
.filter { [unowned self] in $0 == self.statusID }
|
||||
statusUpdater = mastodonController.cache.statusSubject
|
||||
.filter { [unowned self] in $0.id == self.statusID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [unowned self] in
|
||||
if let status = self.mastodonController.persistentContainer.status(for: $0) {
|
||||
self.updateStatusState(status: status)
|
||||
}
|
||||
}
|
||||
.sink { [unowned self] in self.updateStatusState(status: $0) }
|
||||
}
|
||||
|
||||
|
||||
if accountUpdater == nil {
|
||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [unowned self] in $0 == self.accountID }
|
||||
accountUpdater = mastodonController.cache.accountSubject
|
||||
.filter { [unowned self] in $0.id == self.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [unowned self] in
|
||||
if let account = self.mastodonController.persistentContainer.account(for: $0) {
|
||||
self.updateUI(account: account)
|
||||
}
|
||||
}
|
||||
.sink { [unowned self] in self.updateUI(account: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI(statusID: String, state: StatusState) {
|
||||
createObserversIfNecessary()
|
||||
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
guard let status = mastodonController.cache.status(for: statusID) else {
|
||||
fatalError("Missing cached status")
|
||||
}
|
||||
self.statusID = statusID
|
||||
|
@ -164,9 +161,9 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
favorited = status.favourited
|
||||
reblogged = status.reblogged
|
||||
func updateStatusState(status: Status) {
|
||||
favorited = status.favourited ?? false
|
||||
reblogged = status.reblogged ?? false
|
||||
|
||||
if favorited {
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
||||
|
@ -180,22 +177,22 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
func updateUI(account: AccountMO) {
|
||||
func updateUI(account: Account) {
|
||||
usernameLabel.text = "@\(account.acct)"
|
||||
avatarImageView.image = nil
|
||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == account.id else { return }
|
||||
DispatchQueue.main.async {
|
||||
guard let self = self, let data = data, self.accountID == account.id else { return }
|
||||
self.avatarImageView.image = UIImage(data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
|
||||
guard let mastodonController = mastodonController, let account = mastodonController.cache.account(for: accountID) else { return }
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
|
||||
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
|
@ -251,18 +248,18 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
@IBAction func favoritePressed() {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
|
||||
let oldValue = favorited
|
||||
favorited = !favorited
|
||||
|
||||
let realStatus = status.reblog ?? status
|
||||
let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus.id)
|
||||
let realStatus: Status = status.reblog ?? status
|
||||
let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus)
|
||||
mastodonController.run(request) { response in
|
||||
DispatchQueue.main.async {
|
||||
if case let .success(newStatus, _) = response {
|
||||
self.favorited = newStatus.favourited
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||
self.favorited = newStatus.favourited ?? false
|
||||
self.mastodonController.cache.add(status: newStatus)
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
} else {
|
||||
self.favorited = oldValue
|
||||
|
@ -276,18 +273,18 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
@IBAction func reblogPressed() {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
|
||||
let oldValue = reblogged
|
||||
reblogged = !reblogged
|
||||
|
||||
let realStatus = status.reblog ?? status
|
||||
let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus.id)
|
||||
let realStatus: Status = status.reblog ?? status
|
||||
let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus)
|
||||
mastodonController.run(request) { response in
|
||||
DispatchQueue.main.async {
|
||||
if case let .success(newStatus, _) = response {
|
||||
self.reblogged = newStatus.reblogged
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||
self.reblogged = newStatus.reblogged ?? false
|
||||
self.mastodonController.cache.add(status: newStatus)
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
} else {
|
||||
self.reblogged = oldValue
|
||||
|
@ -314,7 +311,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
|
||||
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
||||
func attachmentViewGallery(startingAt index: Int) -> UIViewController {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
||||
return delegate!.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
||||
}
|
||||
|
|
|
@ -40,16 +40,16 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
|||
|
||||
override func updateUI(statusID: String, state: StatusState) {
|
||||
super.updateUI(statusID: statusID, state: state)
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError() }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError() }
|
||||
|
||||
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
|
||||
if let application = status.applicationName {
|
||||
timestampAndClientText += " • \(application)"
|
||||
if let application = status.application {
|
||||
timestampAndClientText += " • \(application.name)"
|
||||
}
|
||||
timestampAndClientLabel.text = timestampAndClientText
|
||||
}
|
||||
|
||||
override func updateStatusState(status: StatusMO) {
|
||||
override func updateStatusState(status: Status) {
|
||||
super.updateStatusState(status: status)
|
||||
|
||||
// todo: localize me
|
||||
|
@ -57,7 +57,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
|||
totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal)
|
||||
}
|
||||
|
||||
override func updateUI(account: AccountMO) {
|
||||
override func updateUI(account: Account) {
|
||||
super.updateUI(account: account)
|
||||
|
||||
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji
|
||||
|
|
|
@ -48,22 +48,19 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
super.createObserversIfNecessary()
|
||||
|
||||
if rebloggerAccountUpdater == nil {
|
||||
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [unowned self] in $0 == self.rebloggerID }
|
||||
rebloggerAccountUpdater = mastodonController.cache.accountSubject
|
||||
.filter { [unowned self] in $0.id == self.rebloggerID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [unowned self] in
|
||||
if let reblogger = self.mastodonController.persistentContainer.account(for: $0) {
|
||||
self.updateRebloggerLabel(reblogger: reblogger)
|
||||
}
|
||||
}
|
||||
.sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
override func updateUI(statusID: String, state: StatusState) {
|
||||
guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||
guard var status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||
|
||||
let realStatusID: String
|
||||
if let rebloggedStatus = status.reblog {
|
||||
if let rebloggedStatusID = status.reblog?.id,
|
||||
let rebloggedStatus = mastodonController.cache.status(for: rebloggedStatusID) {
|
||||
reblogStatusID = statusID
|
||||
rebloggerID = status.account.id
|
||||
status = rebloggedStatus
|
||||
|
@ -88,12 +85,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
@objc override func updateUIForPreferences() {
|
||||
super.updateUIForPreferences()
|
||||
if let rebloggerID = rebloggerID,
|
||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||
let reblogger = mastodonController.cache.account(for: rebloggerID) {
|
||||
updateRebloggerLabel(reblogger: reblogger)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRebloggerLabel(reblogger: AccountMO) {
|
||||
private func updateRebloggerLabel(reblogger: Account) {
|
||||
if Preferences.shared.hideCustomEmojiInUsernames {
|
||||
reblogLabel.text = "Reblogged by \(reblogger.displayNameWithoutCustomEmoji)"
|
||||
reblogLabel.removeEmojis()
|
||||
|
@ -107,7 +104,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
|
||||
// so we bail out immediately, since there's nothing to update
|
||||
guard let mastodonController = mastodonController else { return }
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
|
||||
timestampLabel.text = status.createdAt.timeAgoString()
|
||||
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
|
||||
|
@ -136,7 +133,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
func reply() {
|
||||
if Preferences.shared.mentionReblogger,
|
||||
let rebloggerID = rebloggerID,
|
||||
let rebloggerAccount = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||
let rebloggerAccount = mastodonController.cache.account(for: rebloggerID) {
|
||||
delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct)
|
||||
} else {
|
||||
delegate?.reply(to: statusID)
|
||||
|
@ -179,19 +176,19 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
|||
|
||||
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
||||
guard let mastodonController = mastodonController else { return nil }
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||
|
||||
let favoriteTitle: String
|
||||
let favoriteRequest: Request<Status>
|
||||
let favoriteColor: UIColor
|
||||
if status.favourited {
|
||||
if status.favourited ?? false {
|
||||
favoriteTitle = "Unfavorite"
|
||||
favoriteRequest = Status.unfavourite(status.id)
|
||||
favoriteRequest = Status.unfavourite(status)
|
||||
favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
|
||||
|
||||
} else {
|
||||
favoriteTitle = "Favorite"
|
||||
favoriteRequest = Status.favourite(status.id)
|
||||
favoriteRequest = Status.favourite(status)
|
||||
favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1)
|
||||
}
|
||||
let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in
|
||||
|
@ -202,7 +199,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
|||
return
|
||||
}
|
||||
completion(true)
|
||||
mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
mastodonController.cache.add(status: status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -212,13 +209,13 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
|||
let reblogTitle: String
|
||||
let reblogRequest: Request<Status>
|
||||
let reblogColor: UIColor
|
||||
if status.reblogged {
|
||||
if status.reblogged ?? false {
|
||||
reblogTitle = "Unreblog"
|
||||
reblogRequest = Status.unreblog(status.id)
|
||||
reblogRequest = Status.unreblog(status)
|
||||
reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
|
||||
} else {
|
||||
reblogTitle = "Reblog"
|
||||
reblogRequest = Status.reblog(status.id)
|
||||
reblogRequest = Status.reblog(status)
|
||||
reblogColor = tintColor
|
||||
}
|
||||
let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in
|
||||
|
@ -229,7 +226,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
|||
return
|
||||
}
|
||||
completion(true)
|
||||
mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
mastodonController.cache.add(status: status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ class StatusContentTextView: ContentTextView {
|
|||
didSet {
|
||||
guard let statusID = statusID else { return }
|
||||
guard let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
let status = mastodonController.cache.status(for: statusID) else {
|
||||
fatalError("Can't set StatusContentTextView text without cached status for \(statusID)")
|
||||
}
|
||||
setTextFromHtml(status.content)
|
||||
|
@ -27,7 +27,7 @@ class StatusContentTextView: ContentTextView {
|
|||
let mention: Mention?
|
||||
if let statusID = statusID,
|
||||
let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) {
|
||||
let status = mastodonController.cache.status(for: statusID) {
|
||||
mention = status.mentions.first { (mention) in
|
||||
// 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
|
||||
|
@ -42,7 +42,7 @@ class StatusContentTextView: ContentTextView {
|
|||
let hashtag: Hashtag?
|
||||
if let statusID = statusID,
|
||||
let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) {
|
||||
let status = mastodonController.cache.status(for: statusID) {
|
||||
hashtag = status.hashtags.first { (hashtag) in
|
||||
hashtag.url == url
|
||||
}
|
||||
|
|
|
@ -38,62 +38,62 @@ struct XCBActions {
|
|||
|
||||
private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) {
|
||||
if let id = request.arguments["statusID"] {
|
||||
let request = Client.getStatus(id: id)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(status, _) = response else {
|
||||
mastodonController.cache.status(for: id) { (status) in
|
||||
if let status = status {
|
||||
completion(status)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not get status with ID \(id)"
|
||||
])
|
||||
return
|
||||
])
|
||||
}
|
||||
completion(status)
|
||||
}
|
||||
} else if let searchQuery = request.arguments["statusURL"] {
|
||||
let request = Client.search(query: searchQuery)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(results, _) = response,
|
||||
let status = results.statuses.first {
|
||||
mastodonController.cache.add(status: status)
|
||||
completion(status)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not find status by searching '\(searchQuery)'"
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "No status provided. Specify either instance-local statusID or remote statusURL."
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) {
|
||||
if let id = request.arguments["accountID"] {
|
||||
let request = Client.getAccount(id: id)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(account, _) = response else {
|
||||
mastodonController.cache.account(for: id) { (account) in
|
||||
if let account = account {
|
||||
completion(account)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not get account with ID \(id)"
|
||||
])
|
||||
return
|
||||
])
|
||||
}
|
||||
completion(account)
|
||||
}
|
||||
} else if let searchQuery = request.arguments["accountURL"] {
|
||||
let request = Client.search(query: searchQuery)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(results, _) = response {
|
||||
if let account = results.accounts.first {
|
||||
mastodonController.cache.add(account: account)
|
||||
completion(account)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not find account by searching '\(searchQuery)'"
|
||||
])
|
||||
])
|
||||
}
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
} else if let acct = request.arguments["acct"] {
|
||||
|
@ -101,22 +101,23 @@ struct XCBActions {
|
|||
mastodonController.run(request) { (response) in
|
||||
if case let .success(accounts, _) = response {
|
||||
if let account = accounts.first {
|
||||
mastodonController.cache.add(account: account)
|
||||
completion(account)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not find account \(acct)"
|
||||
])
|
||||
])
|
||||
}
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "No status provided. Specify either instance-local ID, account URL, or qualified username."
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,7 +142,7 @@ struct XCBActions {
|
|||
guard CharacterCounter.count(text: status) <= mastodonController.instance.maxStatusCharacters ?? 500 else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Too many characters. Instance maximum is \(mastodonController.instance.maxStatusCharacters ?? 500)"
|
||||
])
|
||||
])
|
||||
return
|
||||
}
|
||||
let request = Client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility)
|
||||
|
@ -150,11 +151,11 @@ struct XCBActions {
|
|||
session.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
])
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -178,7 +179,7 @@ struct XCBActions {
|
|||
} catch {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +192,7 @@ struct XCBActions {
|
|||
"posted": status.createdAt.timeIntervalSince1970.description,
|
||||
"content": content,
|
||||
"reblog": status.reblog?.id
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,19 +204,20 @@ struct XCBActions {
|
|||
statusAction(request: Status.reblog, alertTitle: "Reblog status?", request, session, silent)
|
||||
}
|
||||
|
||||
static func statusAction(request: @escaping (String) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
static func statusAction(request: @escaping (Status) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
func performAction(status: Status, completion: ((Status) -> Void)?) {
|
||||
mastodonController.run(request(status.id)) { (response) in
|
||||
mastodonController.run(request(status)) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
mastodonController.cache.add(status: status)
|
||||
completion?(status)
|
||||
session.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
])
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,7 +271,7 @@ struct XCBActions {
|
|||
"url": account.url.absoluteString,
|
||||
"avatarURL": account.avatar.absoluteString,
|
||||
"headerURL": account.header.absoluteString
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,21 +286,22 @@ struct XCBActions {
|
|||
"url": account.url.absoluteString,
|
||||
"avatarURL": account.avatar.absoluteString,
|
||||
"headerURL": account.header.absoluteString
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
func performAction(_ account: Account) {
|
||||
let request = Account.follow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case .success(_, _) = response {
|
||||
if case let .success(relationship, _) = response {
|
||||
mastodonController.cache.add(relationship: relationship)
|
||||
session.complete(with: .success, additionalData: [
|
||||
"url": account.url.absoluteString
|
||||
])
|
||||
])
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,8 +314,7 @@ struct XCBActions {
|
|||
DispatchQueue.main.async {
|
||||
show(vc)
|
||||
}
|
||||
// todo: update to use managed objects
|
||||
let alertController = UIAlertController(title: "Follow \(account.displayName)?", message: nil, preferredStyle: .alert)
|
||||
let alertController = UIAlertController(title: "Follow \(account.displayNameWithoutCustomEmoji)?", message: nil, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
|
||||
performAction(account)
|
||||
}))
|
||||
|
|
Loading…
Reference in New Issue