From a6d23d12eb64baf5b47946cd3a4b6346972a7d61 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 17 Sep 2018 21:57:46 -0400 Subject: [PATCH] Add shared status cache --- Pachyderm/Client.swift | 4 +- Tusker.xcodeproj/project.pbxproj | 4 ++ .../UIViewController+Delegates.swift | 14 +++-- .../Compose/ComposeViewController.swift | 18 +++--- .../ConversationViewController.swift | 36 ++++++------ .../NotificationsTableViewController.swift | 4 +- .../Profile/ProfileTableViewController.swift | 19 ++++--- .../TimelineTableViewController.swift | 26 ++++----- Tusker/StatusCache.swift | 44 +++++++++++++++ .../ActionNotificationTableViewCell.swift | 15 +++-- .../ConversationMainStatusTableViewCell.swift | 20 +++++-- Tusker/Views/Status/StatusTableViewCell.swift | 55 ++++++++++++------- Tusker/Views/StatusContentLabel.swift | 5 +- 13 files changed, 175 insertions(+), 89 deletions(-) create mode 100644 Tusker/StatusCache.swift diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 3fb9faf6..502ede8b 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -282,7 +282,7 @@ public class Client { } public func createStatus(text: String, - inReplyTo: Status? = nil, + inReplyTo: String? = nil, media: [Attachment]? = nil, sensitive: Bool? = nil, spoilerText: String? = nil, @@ -290,7 +290,7 @@ public class Client { language: String? = nil) -> Request { return Request(method: .post, path: "/api/v1/statuses", body: .parameters([ "status" => text, - "in_reply_to_id" => inReplyTo?.id, + "in_reply_to_id" => inReplyTo, "sensitive" => sensitive, "spoiler_text" => spoilerText, "visibility" => visiblity?.rawValue, diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index b0488939..5044ff09 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; + D6028B9B2150811100F223B9 /* StatusCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* StatusCache.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, ); }; }; @@ -164,6 +165,7 @@ 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; + D6028B9A2150811100F223B9 /* StatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCache.swift; sourceTree = ""; }; D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = ""; }; D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -613,6 +615,7 @@ D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */, + D6028B9A2150811100F223B9 /* StatusCache.swift */, D663626021360A9600C9CBA2 /* Preferences */, D667E5F62135C2ED0057A976 /* Extensions */, D6F953F121251A2F00CF0F2B /* Controllers */, @@ -922,6 +925,7 @@ D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, + D6028B9B2150811100F223B9 /* StatusCache.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, diff --git a/Tusker/Extensions/UIViewController+Delegates.swift b/Tusker/Extensions/UIViewController+Delegates.swift index fb1d4a0a..395731b8 100644 --- a/Tusker/Extensions/UIViewController+Delegates.swift +++ b/Tusker/Extensions/UIViewController+Delegates.swift @@ -39,22 +39,22 @@ extension StatusTableViewCellDelegate where Self: UIViewController { present(vc, animated: true) } - func selected(status: Status) { + func selected(status statusID: String) { // don't open if the conversation is the same as the current one if let conversationController = self as? ConversationViewController, - conversationController.mainStatus == status { + conversationController.mainStatusID == statusID { return } guard let navigationController = navigationController else { fatalError("Can't show conversation VC when not in navigation controller") } - let vc = ConversationViewController.create(for: status) + let vc = ConversationViewController.create(for: statusID) navigationController.pushViewController(vc, animated: true) } - func reply(to status: Status) { - let vc = ComposeViewController.create(inReplyTo: status) + func reply(to statusID: String) { + let vc = ComposeViewController.create(inReplyTo: statusID) present(vc, animated: true) } @@ -77,7 +77,9 @@ extension StatusTableViewCellDelegate where Self: UIViewController { present(vc, animated: true) } - func showMoreOptions(status: Status) { + func showMoreOptions(status statusID: String) { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID)") } + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) if let url = status.url { alert.addAction(UIAlertAction(title: "Open in Safari...", style: .default, handler: { _ in diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 6aafef59..16e92e19 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -11,10 +11,10 @@ import Pachyderm class ComposeViewController: UIViewController { - static func create(inReplyTo: Status? = nil, mentioning: Account? = nil) -> UIViewController { + static func create(inReplyTo inReplyToID: String? = nil, mentioning: Account? = nil) -> UIViewController { guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController, let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() } - composeVC.inReplyTo = inReplyTo + composeVC.inReplyToID = inReplyToID composeVC.mentioning = mentioning return navigationController } @@ -36,7 +36,7 @@ class ComposeViewController: UIViewController { var scrolled = false - var inReplyTo: Status? + var inReplyToID: String? var mentioning: Account? var contentWarning = false { @@ -68,10 +68,11 @@ class ComposeViewController: UIViewController { toolbar.sizeToFit() statusTextView.inputAccessoryView = toolbar - if let inReplyTo = inReplyTo { + if let inReplyToID = inReplyToID, + let inReplyTo = StatusCache.get(id: inReplyToID) { inReplyToDisplayNameLabel.text = inReplyTo.account.realDisplayName inReplyToUsernameLabel.text = "@\(inReplyTo.account.username)" - inReplyToContentLabel.status = inReplyTo + inReplyToContentLabel.statusID = inReplyToID inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView) inReplyToAvatarImageView.layer.masksToBounds = true inReplyToAvatarImageView.image = nil @@ -103,7 +104,7 @@ class ComposeViewController: UIViewController { } override func viewDidLayoutSubviews() { - if inReplyTo != nil && !scrolled { + if inReplyToID != nil && !scrolled { scrollView.contentOffset = CGPoint(x: 0, y: inReplyToContainerView.bounds.height - 44) scrolled = true } @@ -126,7 +127,7 @@ class ComposeViewController: UIViewController { guard let dest = segue.destination as? MainTabBarViewController, let navController = dest.selectedViewController as? UINavigationController, let topVC = navController.topViewController as? StatusTableViewCellDelegate else { return } - topVC.selected(status: status) + topVC.selected(status: status.id) } } @@ -219,7 +220,7 @@ class ComposeViewController: UIViewController { let attachments = attachments.compactMap { $0 } let request = MastodonController.shared.client.createStatus(text: text, - inReplyTo: self.inReplyTo, + inReplyTo: self.inReplyToID, media: attachments, sensitive: sensitive, spoilerText: contentWarning, @@ -227,6 +228,7 @@ class ComposeViewController: UIViewController { MastodonController.shared.client.run(request) { response in guard case let .success(status, _) = response else { fatalError() } self.status = status + StatusCache.add(status) DispatchQueue.main.async { self.progressView.step() self.performSegue(withIdentifier: "postComplete", sender: self) diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index ec1e5bd5..43463f8d 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -11,17 +11,17 @@ import Pachyderm class ConversationViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - static func create(for mainStatus: Status) -> ConversationViewController { + static func create(for mainStatusID: String) -> ConversationViewController { guard let conversationController = UIStoryboard(name: "Conversation", bundle: nil).instantiateInitialViewController() as? ConversationViewController else { fatalError() } - conversationController.mainStatus = mainStatus + conversationController.mainStatusID = mainStatusID return conversationController } @IBOutlet weak var tableView: UITableView! - var mainStatus: Status! + var mainStatusID: String! - var statuses: [Status] = [] { + var statusIDs: [String] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -38,16 +38,18 @@ class ConversationViewController: UIViewController, UITableViewDataSource, UITab tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell") - statuses = [mainStatus] + statusIDs = [mainStatusID] + + guard let mainStatus = StatusCache.get(id: mainStatusID) else { fatalError("Missing cached status \(mainStatusID!)") } let request = Status.getContext(mainStatus) MastodonController.shared.client.run(request) { response in guard case let .success(context, _) = response else { fatalError() } - var statuses = self.getDirectParents(of: self.mainStatus, from: context.ancestors) - statuses.append(self.mainStatus) - statuses.append(contentsOf: context.descendants) - self.statuses = statuses - let indexPath = IndexPath(row: statuses.firstIndex(of: self.mainStatus)!, section: 0) + let parents = self.getDirectParents(of: mainStatus, from: context.ancestors) + StatusCache.addAll(parents) + StatusCache.addAll(context.descendants) + self.statusIDs = parents.map { $0.id } + [self.mainStatusID] + context.descendants.map { $0.id } + let indexPath = IndexPath(row: self.statusIDs.firstIndex(of: self.mainStatusID)!, section: 0) DispatchQueue.main.async { self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) } @@ -94,29 +96,29 @@ class ConversationViewController: UIViewController, UITableViewDataSource, UITab } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return statuses.count + return statusIDs.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let status = statuses[indexPath.row] + let statusID = statusIDs[indexPath.row] - if status == mainStatus { + if statusID == mainStatusID { guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() } cell.selectionStyle = .none - cell.updateUI(for: status) + cell.updateUI(for: statusID) cell.delegate = self return cell } else { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } - cell.updateUI(for: status) + cell.updateUI(for: statusID) cell.delegate = self return cell } } func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - let status = statuses[indexPath.row] - return status == mainStatus ? nil : indexPath + let statusID = statusIDs[indexPath.row] + return statusID == mainStatusID ? nil : indexPath } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index adc8d652..36118b39 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -47,6 +47,7 @@ class NotificationsTableViewController: UITableViewController { MastodonController.shared.client.run(request) { result in guard case let .success(notifications, pagination) = result else { fatalError() } self.notifications = notifications + StatusCache.addAll(notifications.compactMap { $0.status }) self.newer = pagination?.newer self.older = pagination?.older } @@ -90,7 +91,7 @@ class NotificationsTableViewController: UITableViewController { case .mention: guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } let status = notification.status! - cell.updateUI(for: status) + cell.updateUI(for: status.id) cell.delegate = self return cell case .favourite, .reblog: @@ -139,6 +140,7 @@ class NotificationsTableViewController: UITableViewController { MastodonController.shared.client.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } self.newer = pagination?.newer + StatusCache.addAll(newNotifications.compactMap { $0.status }) self.notifications.insert(contentsOf: newNotifications, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 3e9ef06e..6fabc3f3 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -20,7 +20,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { var account: Account! - var statuses: [Status] = [] { + var statusIDs: [String] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -49,7 +49,8 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { getStatuses { response in guard case let .success(statuses, pagination) = response else { fatalError() } - self.statuses = statuses + StatusCache.addAll(statuses) + self.statusIDs = statuses.map { $0.id } self.older = pagination?.older self.newer = pagination?.newer } @@ -96,7 +97,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { case 0: return 1 case 1: - return statuses.count + return statusIDs.count default: return 0 } @@ -112,8 +113,8 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { return cell case 1: guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } - let status = statuses[indexPath.row] - cell.updateUI(for: status) + let statusID = statusIDs[indexPath.row] + cell.updateUI(for: statusID) cell.delegate = self return cell default: @@ -122,13 +123,14 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.section == 1 && indexPath.row == statuses.count - 1 { + if indexPath.section == 1 && indexPath.row == statusIDs.count - 1 { guard let older = older else { return } getStatuses(for: older) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older - self.statuses.append(contentsOf: newStatuses) + StatusCache.addAll(newStatuses) + self.statusIDs.append(contentsOf: newStatuses.map { $0.id }) } } } @@ -151,7 +153,8 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { getStatuses(for: newer) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.newer = pagination?.newer - self.statuses.insert(contentsOf: newStatuses, at: 0) + StatusCache.addAll(newStatuses) + self.statusIDs.insert(contentsOf: newStatuses.map { $0.id }, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() } diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 815b99e8..9bced785 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -50,7 +50,7 @@ class TimelineTableViewController: UITableViewController { var timeline: Timeline! - var statuses: [Status] = [] { + var statusIDs: [String] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -73,7 +73,8 @@ class TimelineTableViewController: UITableViewController { let request = MastodonController.shared.client.getStatuses(timeline: timeline) MastodonController.shared.client.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } - self.statuses = statuses + self.statusIDs = statuses.map { $0.id } + StatusCache.addAll(statuses) self.newer = pagination?.newer self.older = pagination?.older } @@ -106,16 +107,16 @@ class TimelineTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return statuses.count + return statusIDs.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } - let status = statuses[indexPath.row] + let statusID = statusIDs[indexPath.row] - cell.updateUI(for: status) + cell.updateUI(for: statusID) cell.delegate = self @@ -123,14 +124,15 @@ class TimelineTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.row == statuses.count - 1 { + if indexPath.row == statusIDs.count - 1 { guard let older = older else { return } let request = MastodonController.shared.client.getStatuses(timeline: timeline, range: older) MastodonController.shared.client.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older - self.statuses.append(contentsOf: newStatuses) + StatusCache.addAll(newStatuses) + self.statusIDs.append(contentsOf: newStatuses.map { $0.id }) } } } @@ -154,7 +156,8 @@ class TimelineTableViewController: UITableViewController { MastodonController.shared.client.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.newer = pagination?.newer - self.statuses.insert(contentsOf: newStatuses, at: 0) + StatusCache.addAll(newStatuses) + self.statusIDs.insert(contentsOf: newStatuses.map { $0.id }, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() @@ -166,10 +169,5 @@ class TimelineTableViewController: UITableViewController { } -extension TimelineTableViewController: StatusTableViewCellDelegate { - func updatedStatus(for cell: StatusTableViewCell, status: Status) { - let indexPath = tableView.indexPath(for: cell)! - statuses[indexPath.row] = status - } -} +extension TimelineTableViewController: StatusTableViewCellDelegate {} extension TimelineTableViewController: LargeImageViewControllerDelegate {} diff --git a/Tusker/StatusCache.swift b/Tusker/StatusCache.swift new file mode 100644 index 00000000..29872248 --- /dev/null +++ b/Tusker/StatusCache.swift @@ -0,0 +1,44 @@ +// +// StatusCache.swift +// Tusker +// +// Created by Shadowfacts on 9/17/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation +import Pachyderm + +class StatusCache { + + static let cache = NSCache() + + static func get(id: String) -> Status? { + return cache.object(forKey: id as NSString) + } + + static func set(id: String, status: Status) { + cache.setObject(status, forKey: id as NSString) + } + + static func get(id: String, completion: @escaping (Status?) -> Void) { + let request = MastodonController.shared.client.getStatus(id: id) + MastodonController.shared.client.run(request) { response in + guard case let .success(status, _) = response else { + completion(nil) + return + } + set(id: id, status: status) + completion(status) + } + } + + static func add(_ status: Status) { + set(id: status.id, status: status) + } + + static func addAll(_ statuses: [Status]) { + statuses.forEach { set(id: $0.id, status: $0) } + } + +} diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift index a09febf7..29dcc7e4 100644 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift @@ -23,7 +23,7 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { @IBOutlet weak var attachmentsView: UIStackView! var notification: Pachyderm.Notification! - var status: Status! + var statusID: String! var opAvatarURL: URL? var actionAvatarURL: URL? @@ -44,6 +44,8 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { } func updateUIForPreferences() { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + opAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: opAvatarImageView) actionAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: actionAvatarImageView) displayNameLabel.text = status.account.realDisplayName @@ -65,7 +67,8 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { fatalError("Invalid notification type \(notification.kind) for ActionNotificationTableViewCell") } self.notification = notification - self.status = notification.status! + let status = notification.status! + self.statusID = status.id updateUIForPreferences() @@ -126,10 +129,12 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { attachmentsView.isHidden = true } - contentLabel.status = status + contentLabel.statusID = status.id } func updateTimestamp() { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + timestampLabel.text = status.createdAt.timeAgoString() let delay: DispatchTimeInterval? switch status.createdAt.timeAgo().1 { @@ -166,12 +171,12 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { super.setSelected(selected, animated: animated) if selected { - delegate?.selected(status: status) + delegate?.selected(status: statusID) } } @objc func accountPressed() { - delegate?.selected(account: status.account) + delegate?.selected(account: notification.status!.account) } @objc func actionPressed() { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 62cf1f91..e8b32416 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -22,7 +22,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive @IBOutlet weak var favoriteButton: UIButton! @IBOutlet weak var reblogButton: UIButton! - var status: Status! + var statusID: String! var account: Account! var favorited: Bool = false { @@ -57,8 +57,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive displayNameLabel.text = account.realDisplayName } - func updateUI(for status: Status) { - self.status = status + func updateUI(for statusID: String) { + self.statusID = statusID + + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID)") } let account: Account if let reblog = status.reblog { @@ -112,10 +114,12 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive favorited = realStatus.favourited ?? false reblogged = realStatus.reblogged ?? false - contentLabel.status = status + contentLabel.statusID = statusID } func updateTimestamp() { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + timestampLabel.text = status.createdAt.timeAgoString() let delay: DispatchTimeInterval? switch status.createdAt.timeAgo().1 { @@ -158,10 +162,12 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive } @IBAction func replyPressed(_ sender: Any) { - delegate?.reply(to: status) + delegate?.reply(to: statusID) } @IBAction func favoritePressed(_ sender: Any) { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + favorited = !favorited let realStatus: Status = status.reblog ?? status @@ -183,6 +189,8 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive } @IBAction func reblogPressed(_ sender: Any) { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + reblogged = !reblogged let realStatus: Status = status.reblog ?? status @@ -203,7 +211,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive } @IBAction func morePressed(_ sender: Any) { - delegate?.showMoreOptions(status: status) + delegate?.showMoreOptions(status: statusID) } } diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index 21c1e8f3..861e0e24 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -19,13 +19,13 @@ protocol StatusTableViewCellDelegate { func selected(url: URL) - func selected(status: Status) + func selected(status statusID: String) - func reply(to status: Status) + func reply(to statusID: String) func showLargeImage(_ image: UIImage, description: String?, animatingFrom originView: UIView) - func showMoreOptions(status: Status) + func showMoreOptions(status statusID: String) } @@ -43,7 +43,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { @IBOutlet weak var favoriteButton: UIButton! @IBOutlet weak var reblogButton: UIButton! - var status: Status! + var statusID: String! var account: Account! var reblogger: Account? @@ -85,8 +85,9 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { displayNameLabel.text = account.realDisplayName } - func updateUI(for status: Status) { - self.status = status + func updateUI(for statusID: String) { + self.statusID = statusID + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID)") } let account: Account if let reblog = status.reblog { @@ -146,10 +147,12 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { favorited = realStatus.favourited ?? false reblogged = realStatus.reblogged ?? false - contentLabel.status = status + contentLabel.statusID = statusID } func updateTimestamp() { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + timestampLabel.text = status.createdAt.timeAgoString() let delay: DispatchTimeInterval? switch status.createdAt.timeAgo().1 { @@ -192,12 +195,12 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { super.setSelected(selected, animated: animated) if selected { - delegate?.selected(status: status) + delegate?.selected(status: statusID) } } @IBAction func replyPressed(_ sender: Any) { - delegate?.reply(to: status) + delegate?.reply(to: statusID) } @objc func accountPressed() { @@ -210,6 +213,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } @IBAction func favoritePressed(_ sender: Any) { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + let oldValue = favorited favorited = !favorited @@ -233,6 +238,9 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } @IBAction func reblogPressed(_ sender: Any) { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + + let oldValue = reblogged reblogged = !reblogged let realStatus: Status = status.reblog ?? status @@ -240,10 +248,11 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { MastodonController.shared.client.run(request) { response in self.reblogged = realStatus.reblogged ?? false DispatchQueue.main.async { - self.reblogged = realStatus.reblogged ?? false if case .success = response { + self.reblogged = realStatus.reblogged ?? false UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { + self.reblogged = oldValue print("Couldn't reblog status \(realStatus.id)") // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) @@ -253,7 +262,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } @IBAction func morePressed(_ sender: Any) { - delegate?.showMoreOptions(status: status) + delegate?.showMoreOptions(status: statusID) } } @@ -274,6 +283,8 @@ extension StatusTableViewCell: TableViewSwipeActionProvider { } func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { + guard let status = StatusCache.get(id: statusID) else { fatalError("Missing cached status \(statusID!)") } + let favoriteTitle: String let favoriteRequest: Request let favoriteColor: UIColor @@ -290,12 +301,13 @@ extension StatusTableViewCell: TableViewSwipeActionProvider { let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in MastodonController.shared.client.run(favoriteRequest, completion: { response in DispatchQueue.main.async { - if case .success = response { - completion(true) - self.updateUI(for: self.status) - } else { + guard case let .success(status, _) = response else { completion(false) + return } + completion(true) + StatusCache.add(status) + self.updateUI(for: self.statusID) } }) } @@ -317,12 +329,13 @@ extension StatusTableViewCell: TableViewSwipeActionProvider { let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in MastodonController.shared.client.run(reblogRequest, completion: { response in DispatchQueue.main.async { - if case .success = response { - completion(true) - self.updateUI(for: self.status) - } else { + guard case let .success(status, _) = response else { completion(false) + return } + completion(true) + StatusCache.add(status) + self.updateUI(for: self.statusID) } }) } @@ -335,13 +348,13 @@ extension StatusTableViewCell: TableViewSwipeActionProvider { func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in completion(true) - self.delegate?.reply(to: self.status) + self.delegate?.reply(to: self.statusID) } reply.image = StatusTableViewCell.replyActionImage reply.backgroundColor = tintColor let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in completion(true) - self.delegate?.showMoreOptions(status: self.status) + self.delegate?.showMoreOptions(status: self.statusID) } more.image = StatusTableViewCell.moreActionImage more.backgroundColor = .gray diff --git a/Tusker/Views/StatusContentLabel.swift b/Tusker/Views/StatusContentLabel.swift index a30f071b..8c15eff7 100644 --- a/Tusker/Views/StatusContentLabel.swift +++ b/Tusker/Views/StatusContentLabel.swift @@ -11,11 +11,14 @@ import Pachyderm class StatusContentLabel: HTMLContentLabel { - var status: Status! { + var statusID: String! { didSet { text = status.content } } + var status: Status! { + return StatusCache.get(id: statusID) + } override func getMention(for url: URL, text: String) -> Mention? { return status.mentions.first(where: { mention -> Bool in