Start converting UI to use CoreData backed objects instead of API

objects directly
This commit is contained in:
Shadowfacts 2020-04-12 12:54:27 -04:00
parent a0e95d4577
commit 2c8ba878b7
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
21 changed files with 146 additions and 101 deletions

View File

@ -58,7 +58,7 @@ extension AccountMO {
} }
self.acct = account.acct self.acct = account.acct
self.avatar = account.avatar self.avatar = account.avatarStatic // we don't animate avatars
self.bot = account.bot ?? false self.bot = account.bot ?? false
self.createdAt = account.createdAt self.createdAt = account.createdAt
self.displayName = account.displayName self.displayName = account.displayName
@ -66,7 +66,7 @@ extension AccountMO {
self.fields = account.fields ?? [] self.fields = account.fields ?? []
self.followersCount = account.followersCount self.followersCount = account.followersCount
self.followingCount = account.followingCount self.followingCount = account.followingCount
self.header = account.header self.header = account.headerStatic // we don't animate headers
self.id = account.id self.id = account.id
self.locked = account.locked self.locked = account.locked
self.moved = account.moved ?? false self.moved = account.moved ?? false

View File

@ -37,19 +37,33 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
private func upsert(status: Status) {
if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
statusMO.updateFrom(apiStatus: status, container: self)
} else {
_ = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
}
}
func addOrUpdate(status: Status, save: Bool = true) { func addOrUpdate(status: Status, save: Bool = true) {
backgroundContext.perform { backgroundContext.perform {
if let statusMO = self.status(for: status.id, in: self.backgroundContext) { self.upsert(status: status)
statusMO.updateFrom(apiStatus: status, container: self)
} else {
_ = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
}
if save, self.backgroundContext.hasChanges { if save, self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
} }
} }
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
backgroundContext.perform {
statuses.forEach(self.upsert(status:))
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
completion?()
}
}
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? { func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
let context = context ?? viewContext let context = context ?? viewContext
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest() let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
@ -62,17 +76,31 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
private func upsert(account: Account) {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self)
} else {
_ = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
}
}
func addOrUpdate(account: Account, save: Bool = true) { func addOrUpdate(account: Account, save: Bool = true) {
backgroundContext.perform { backgroundContext.perform {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) { self.upsert(account: account)
accountMO.updateFrom(apiAccount: account, container: self)
} else {
_ = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
}
if save, self.backgroundContext.hasChanges { if save, self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
} }
} }
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform {
accounts.forEach(self.upsert(account:))
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
completion?()
}
}
} }

View File

@ -36,6 +36,7 @@ public final class StatusMO: NSManagedObject {
@NSManaged public var reblogged: Bool @NSManaged public var reblogged: Bool
@NSManaged public var reblogsCount: Int @NSManaged public var reblogsCount: Int
@NSManaged public var sensitive: Bool @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 uri: String // todo: are both uri and url necessary?
@NSManaged public var url: URL? @NSManaged public var url: URL?
@NSManaged private var visibilityString: String @NSManaged private var visibilityString: String
@ -95,6 +96,7 @@ extension StatusMO {
self.reblogged = status.reblogged ?? false self.reblogged = status.reblogged ?? false
self.reblogsCount = status.reblogsCount self.reblogsCount = status.reblogsCount
self.sensitive = status.sensitive self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri self.uri = status.uri
self.visibility = status.visibility self.visibility = status.visibility
self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context) self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context)

View File

@ -44,6 +44,7 @@
<attribute name="reblogged" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="reblogged" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" attributeType="String"/>
<attribute name="uri" attributeType="String"/> <attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/> <attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibilityString" attributeType="String"/> <attribute name="visibilityString" attributeType="String"/>
@ -57,6 +58,6 @@
</entity> </entity>
<elements> <elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="313"/> <element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="313"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="388"/> <element name="Status" positionX="-63" positionY="-18" width="128" height="403"/>
</elements> </elements>
</model> </model>

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
extension Account { extension AccountMO {
var displayOrUserName: String { var displayOrUserName: String {
if displayName.isEmpty { if displayName.isEmpty {
@ -31,7 +31,7 @@ extension Account {
private func stripCustomEmoji(from string: String) -> String { private func stripCustomEmoji(from string: String) -> String {
let range = NSRange(location: 0, length: string.utf16.count) let range = NSRange(location: 0, length: string.utf16.count)
return Account.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "") return AccountMO.customEmojiRegex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
} }
} }

View File

@ -11,6 +11,7 @@ import Foundation
private let decoder = PropertyListDecoder() private let decoder = PropertyListDecoder()
private let encoder = PropertyListEncoder() private let encoder = PropertyListEncoder()
// todo: invalidate cache on underlying data change using KVO?
@propertyWrapper @propertyWrapper
struct LazilyDecoding<Enclosing, Value: Codable> { struct LazilyDecoding<Enclosing, Value: Codable> {

View File

@ -64,12 +64,7 @@ class MastodonCache {
func addAll(statuses: [Status]) { func addAll(statuses: [Status]) {
statuses.forEach(add) statuses.forEach(add)
if let container = mastodonController?.persistentContainer { mastodonController?.persistentContainer.addAll(statuses: statuses)
statuses.forEach { container.addOrUpdate(status: $0, save: false) }
container.backgroundContext.perform {
try! container.backgroundContext.save()
}
}
} }
// MARK: - Accounts // MARK: - Accounts
@ -104,12 +99,7 @@ class MastodonCache {
func addAll(accounts: [Account]) { func addAll(accounts: [Account]) {
accounts.forEach(add) accounts.forEach(add)
if let container = mastodonController?.persistentContainer { mastodonController?.persistentContainer.addAll(accounts: accounts)
accounts.forEach { container.addOrUpdate(account: $0, save: false) }
container.backgroundContext.perform {
try! container.backgroundContext.save()
}
}
} }
// MARK: - Relationships // MARK: - Relationships

View File

@ -213,7 +213,8 @@ class ComposeViewController: UIViewController {
replyAvatarImageViewTopConstraint!.isActive = true replyAvatarImageViewTopConstraint!.isActive = true
inReplyToContainer.isHidden = false inReplyToContainer.isHidden = false
inReplyToLabel.text = "In reply to \(inReplyTo.account.displayOrUserName)" // todo: update to use managed objects
inReplyToLabel.text = "In reply to \(inReplyTo.account.displayName)"
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {

View File

@ -128,7 +128,7 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let accountID = accountID, let account = mastodonController.cache.account(for: accountID) else { return } guard let accountID = accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
navigationItem.title = account.displayNameWithoutCustomEmoji navigationItem.title = account.displayNameWithoutCustomEmoji
} }

View File

@ -59,12 +59,15 @@ class TimelineTableViewController: EnhancedTableViewController {
let request = Client.getStatuses(timeline: timeline) let request = Client.getStatuses(timeline: timeline)
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
self.mastodonController.cache.addAll(statuses: statuses) // self.mastodonController.cache.addAll(statuses: statuses)
// todo: possible race condition here? we update the underlying data before waiting to reload the table view
self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0)
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
DispatchQueue.main.async { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
self.tableView.reloadData() DispatchQueue.main.async {
self.tableView.reloadData()
}
} }
} }
} }
@ -101,13 +104,15 @@ class TimelineTableViewController: EnhancedTableViewController {
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.older = pagination?.older self.older = pagination?.older
self.mastodonController.cache.addAll(statuses: newStatuses) // self.mastodonController.cache.addAll(statuses: newStatuses)
let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count) let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count)
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.timelineSegments.count - 1) } 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.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
DispatchQueue.main.async { self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
UIView.performWithoutAnimation { DispatchQueue.main.async {
self.tableView.insertRows(at: newIndexPaths, with: .none) UIView.performWithoutAnimation {
self.tableView.insertRows(at: newIndexPaths, with: .none)
}
} }
} }
} }
@ -133,25 +138,27 @@ class TimelineTableViewController: EnhancedTableViewController {
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.newer = pagination?.newer self.newer = pagination?.newer
self.mastodonController.cache.addAll(statuses: newStatuses) // self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
if let newer = pagination?.newer { if let newer = pagination?.newer {
self.newer = newer self.newer = newer
} }
DispatchQueue.main.async { self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
let newIndexPaths = (0..<newStatuses.count).map { DispatchQueue.main.async {
IndexPath(row: $0, section: 0) 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)
} }
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)
} }
} }
} }
@ -175,7 +182,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
extension TimelineTableViewController: UITableViewDataSourcePrefetching { extension TimelineTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
_ = ImageCache.avatars.get(status.account.avatar, completion: nil) _ = ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
_ = ImageCache.attachments.get(attachment.url, completion: nil) _ = ImageCache.attachments.get(attachment.url, completion: nil)
@ -185,7 +192,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
ImageCache.avatars.cancelWithoutCallback(status.account.avatar) ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.cancelWithoutCallback(attachment.url) ImageCache.attachments.cancelWithoutCallback(attachment.url)

View File

@ -32,12 +32,13 @@ class UserActivityManager {
// MARK: - New Post // MARK: - New Post
static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity { static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity {
// todo: update to use managed objects
let activity = NSUserActivity(type: .newPost) let activity = NSUserActivity(type: .newPost)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
if let mentioning = mentioning { if let mentioning = mentioning {
activity.userInfo = ["mentioning": mentioning.acct] activity.userInfo = ["mentioning": mentioning.acct]
activity.title = "Send a message to \(mentioning.displayOrUserName)" activity.title = "Send a message to \(mentioning.displayName)"
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayOrUserName)" activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)"
} else { } else {
activity.userInfo = [:] activity.userInfo = [:]
activity.title = "New Post" activity.title = "New Post"

View File

@ -48,7 +48,7 @@ class AttachmentsContainerView: UIView {
// MARK: - User Interaface // MARK: - User Interaface
func updateUI(status: Status) { func updateUI(status: StatusMO) {
self.statusID = status.id self.statusID = status.id
attachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) } attachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) }

View File

@ -83,6 +83,15 @@ class EmojiLabel: UILabel {
extension EmojiLabel { extension EmojiLabel {
func updateForAccountDisplayName(account: Account) { 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 { if Preferences.shared.hideCustomEmojiInUsernames {
self.text = account.displayNameWithoutCustomEmoji self.text = account.displayNameWithoutCustomEmoji
self.removeEmojis() self.removeEmojis()

View File

@ -138,13 +138,14 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
let peopleStr: String let peopleStr: String
// todo: figure out how to localize this // todo: figure out how to localize this
// todo: update to use managed objects
switch people.count { switch people.count {
case 1: case 1:
peopleStr = people.first!.displayOrUserName peopleStr = people.first!.displayName
case 2: case 2:
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName peopleStr = people.first!.displayName + " and " + people.last!.displayName
default: default:
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
} }
actionLabel.text = "\(verb) by \(peopleStr)" actionLabel.text = "\(verb) by \(peopleStr)"
} }

View File

@ -72,15 +72,16 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
func updateActionLabel(people: [Account]) { func updateActionLabel(people: [Account]) {
// todo: update to use managed objects
// todo: figure out how to localize this // todo: figure out how to localize this
let peopleStr: String let peopleStr: String
switch people.count { switch people.count {
case 1: case 1:
peopleStr = people.first!.displayOrUserName peopleStr = people.first!.displayName
case 2: case 2:
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName peopleStr = people.first!.displayName + " and " + people.last!.displayName
default: default:
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
} }
actionLabel.text = "Followed by \(peopleStr)" actionLabel.text = "Followed by \(peopleStr)"

View File

@ -52,12 +52,13 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
} }
func updateUI(account: Account) { func updateUI(account: Account) {
// todo: update to use managed objects
self.account = account self.account = account
if Preferences.shared.hideCustomEmojiInUsernames { if Preferences.shared.hideCustomEmojiInUsernames {
actionLabel.text = "Request to follow from \(account.displayNameWithoutCustomEmoji)" actionLabel.text = "Request to follow from \(account.displayName)"
actionLabel.removeEmojis() actionLabel.removeEmojis()
} else { } else {
actionLabel.text = "Request to follow from \(account.displayOrUserName)" actionLabel.text = "Request to follow from \(account.displayName)"
actionLabel.setEmojis(account.emojis, identifier: account.id) actionLabel.setEmojis(account.emojis, identifier: account.id)
} }
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in

View File

@ -95,25 +95,26 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
open func createObserversIfNecessary() { open func createObserversIfNecessary() {
if statusUpdater == nil { // todo: KVO on StatusMO for this?
statusUpdater = mastodonController.cache.statusSubject // if statusUpdater == nil {
.filter { [unowned self] in $0.id == self.statusID } // statusUpdater = mastodonController.cache.statusSubject
.receive(on: DispatchQueue.main) // .filter { [unowned self] in $0.id == self.statusID }
.sink { [unowned self] in self.updateStatusState(status: $0) } // .receive(on: DispatchQueue.main)
} // .sink { [unowned self] in self.updateStatusState(status: $0) }
// }
if accountUpdater == nil { //
accountUpdater = mastodonController.cache.accountSubject // if accountUpdater == nil {
.filter { [unowned self] in $0.id == self.accountID } // accountUpdater = mastodonController.cache.accountSubject
.receive(on: DispatchQueue.main) // .filter { [unowned self] in $0.id == self.accountID }
.sink { [unowned self] in self.updateUI(account: $0) } // .receive(on: DispatchQueue.main)
} // .sink { [unowned self] in self.updateUI(account: $0) }
// }
} }
func updateUI(statusID: String, state: StatusState) { func updateUI(statusID: String, state: StatusState) {
createObserversIfNecessary() createObserversIfNecessary()
guard let status = mastodonController.cache.status(for: statusID) else { guard let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Missing cached status") fatalError("Missing cached status")
} }
self.statusID = statusID self.statusID = statusID
@ -161,9 +162,9 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
func updateStatusState(status: Status) { func updateStatusState(status: StatusMO) {
favorited = status.favourited ?? false favorited = status.favourited
reblogged = status.reblogged ?? false reblogged = status.reblogged
if favorited { if favorited {
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label") favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
@ -177,22 +178,22 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
func updateUI(account: Account) { func updateUI(account: AccountMO) {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in 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 { DispatchQueue.main.async {
guard let self = self, let data = data, self.accountID == account.id else { return }
self.avatarImageView.image = UIImage(data: data) self.avatarImageView.image = UIImage(data: data)
} }
} }
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let mastodonController = mastodonController, let account = mastodonController.cache.account(for: accountID) else { return } guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false) attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
} }
override func prepareForReuse() { override func prepareForReuse() {
@ -311,7 +312,7 @@ class BaseStatusTableViewCell: UITableViewCell {
extension BaseStatusTableViewCell: AttachmentViewDelegate { extension BaseStatusTableViewCell: AttachmentViewDelegate {
func attachmentViewGallery(startingAt index: Int) -> UIViewController { func attachmentViewGallery(startingAt index: Int) -> UIViewController {
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
return delegate!.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) return delegate!.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
} }

View File

@ -49,7 +49,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
timestampAndClientLabel.text = timestampAndClientText timestampAndClientLabel.text = timestampAndClientText
} }
override func updateStatusState(status: Status) { override func updateStatusState(status: StatusMO) {
super.updateStatusState(status: status) super.updateStatusState(status: status)
// todo: localize me // todo: localize me
@ -57,7 +57,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal) totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal)
} }
override func updateUI(account: Account) { override func updateUI(account: AccountMO) {
super.updateUI(account: account) super.updateUI(account: account)
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji

View File

@ -47,20 +47,20 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
override func createObserversIfNecessary() { override func createObserversIfNecessary() {
super.createObserversIfNecessary() super.createObserversIfNecessary()
if rebloggerAccountUpdater == nil { // todo: use KVO on reblogger account?
rebloggerAccountUpdater = mastodonController.cache.accountSubject // if rebloggerAccountUpdater == nil {
.filter { [unowned self] in $0.id == self.rebloggerID } // rebloggerAccountUpdater = mastodonController.cache.accountSubject
.receive(on: DispatchQueue.main) // .filter { [unowned self] in $0.id == self.rebloggerID }
.sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) } // .receive(on: DispatchQueue.main)
} // .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) }
// }
} }
override func updateUI(statusID: String, state: StatusState) { override func updateUI(statusID: String, state: StatusState) {
guard var status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard var status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String let realStatusID: String
if let rebloggedStatusID = status.reblog?.id, if let rebloggedStatus = status.reblog {
let rebloggedStatus = mastodonController.cache.status(for: rebloggedStatusID) {
reblogStatusID = statusID reblogStatusID = statusID
rebloggerID = status.account.id rebloggerID = status.account.id
status = rebloggedStatus status = rebloggedStatus
@ -77,7 +77,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateTimestamp() updateTimestamp()
let pinned = status.pinned ?? false let pinned = status.pinned
pinImageView.isHidden = !(pinned && showPinned) pinImageView.isHidden = !(pinned && showPinned)
timestampLabel.isHidden = !pinImageView.isHidden timestampLabel.isHidden = !pinImageView.isHidden
} }
@ -85,12 +85,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
@objc override func updateUIForPreferences() { @objc override func updateUIForPreferences() {
super.updateUIForPreferences() super.updateUIForPreferences()
if let rebloggerID = rebloggerID, if let rebloggerID = rebloggerID,
let reblogger = mastodonController.cache.account(for: rebloggerID) { let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger) updateRebloggerLabel(reblogger: reblogger)
} }
} }
private func updateRebloggerLabel(reblogger: Account) { private func updateRebloggerLabel(reblogger: AccountMO) {
if Preferences.shared.hideCustomEmojiInUsernames { if Preferences.shared.hideCustomEmojiInUsernames {
reblogLabel.text = "Reblogged by \(reblogger.displayNameWithoutCustomEmoji)" reblogLabel.text = "Reblogged by \(reblogger.displayNameWithoutCustomEmoji)"
reblogLabel.removeEmojis() reblogLabel.removeEmojis()
@ -104,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 // 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 // so we bail out immediately, since there's nothing to update
guard let mastodonController = mastodonController else { return } guard let mastodonController = mastodonController else { return }
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date()) timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())

View File

@ -15,7 +15,7 @@ class StatusContentTextView: ContentTextView {
didSet { didSet {
guard let statusID = statusID else { return } guard let statusID = statusID else { return }
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) else { let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Can't set StatusContentTextView text without cached status for \(statusID)") fatalError("Can't set StatusContentTextView text without cached status for \(statusID)")
} }
setTextFromHtml(status.content) setTextFromHtml(status.content)

View File

@ -314,7 +314,8 @@ struct XCBActions {
DispatchQueue.main.async { DispatchQueue.main.async {
show(vc) show(vc)
} }
let alertController = UIAlertController(title: "Follow \(account.displayNameWithoutCustomEmoji)?", message: nil, preferredStyle: .alert) // todo: update to use managed objects
let alertController = UIAlertController(title: "Follow \(account.displayName)?", message: nil, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
performAction(account) performAction(account)
})) }))