From b47b08fa95e9333d950f7a03e472c324027a8fcf Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 28 Nov 2019 18:36:58 -0500 Subject: [PATCH] Store status collapse state in containing view controller Also, copy the state between screens, so e.g. expanding a status in the timeline and then opening that conversation already has that status expanded. This intentionally doesn't store the sensitive attachment visibility state, since showing text when not necessary is less dangerous than for images. (Possibly a preference for this in the future?) Closes #55 --- Pachyderm/Utilities/NotificationGroup.swift | 6 +++ Pachyderm/Utilities/StatusState.swift | 40 +++++++++++++++ Tusker.xcodeproj/project.pbxproj | 4 ++ .../ConversationTableViewController.swift | 43 +++++++++------- .../NotificationsTableViewController.swift | 4 +- .../Profile/ProfileTableViewController.swift | 28 +++++------ .../Search/SearchTableViewController.swift | 14 +++--- ...ActionAccountListTableViewController.swift | 8 +-- .../TimelineTableViewController.swift | 15 +++--- Tusker/TuskerNavigationDelegate.swift | 15 ++++-- ...ActionNotificationGroupTableViewCell.swift | 4 +- .../Status/BaseStatusTableViewCell.swift | 49 ++++++++++++------- .../ConversationMainStatusTableViewCell.swift | 8 +-- .../Status/TimelineStatusTableViewCell.swift | 8 +-- 14 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 Pachyderm/Utilities/StatusState.swift diff --git a/Pachyderm/Utilities/NotificationGroup.swift b/Pachyderm/Utilities/NotificationGroup.swift index 7907b3c7..b53ea992 100644 --- a/Pachyderm/Utilities/NotificationGroup.swift +++ b/Pachyderm/Utilities/NotificationGroup.swift @@ -12,12 +12,18 @@ public class NotificationGroup { 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.notificationIDs = notifications.map { $0.id } self.id = notifications.first!.id self.kind = notifications.first!.kind + if kind == .mention { + self.statusState = .unknown + } else { + self.statusState = nil + } } public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { diff --git a/Pachyderm/Utilities/StatusState.swift b/Pachyderm/Utilities/StatusState.swift new file mode 100644 index 00000000..c3b380c9 --- /dev/null +++ b/Pachyderm/Utilities/StatusState.swift @@ -0,0 +1,40 @@ +// +// StatusState.swift +// Pachyderm +// +// Created by Shadowfacts on 11/24/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation + +public class StatusState: Equatable, Hashable { + public var collapsible: Bool? + public var collapsed: Bool? + + public var unknown: Bool { + collapsible == nil || collapsed == nil + } + + public init(collapsible: Bool?, collapsed: Bool?) { + self.collapsible = collapsible + self.collapsed = collapsed + } + + public func copy() -> StatusState { + return StatusState(collapsible: self.collapsible, collapsed: self.collapsed) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(collapsible) + hasher.combine(collapsed) + } + + public static var unknown: StatusState { + StatusState(collapsible: nil, collapsed: nil) + } + + public static func == (lhs: StatusState, rhs: StatusState) -> Bool { + lhs.collapsible == rhs.collapsible && lhs.collapsed == rhs.collapsed + } +} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 1b581a18..851bd5fa 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; }; 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 */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; }; @@ -285,6 +286,7 @@ 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = ""; }; + D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = ""; }; D60A548B21ED515800F1F87C /* GMImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GMImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D60A548D21ED515800F1F87C /* GMImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GMImagePicker.h; sourceTree = ""; }; D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1000,6 +1002,7 @@ D6A3BC7223218C6E00FD64D5 /* Utilities */ = { isa = PBXGroup; children = ( + D60A4FFB238B726A008AC647 /* StatusState.swift */, D6E6F26221603F8B006A8599 /* CharacterCounter.swift */, D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */, D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */, @@ -1534,6 +1537,7 @@ D61099F5214568C300432DC2 /* Notification.swift in Sources */, D61099EF214566C000432DC2 /* Instance.swift in Sources */, D61099D22144B2E600432DC2 /* Body.swift in Sources */, + D63569E023908A8D003DD353 /* StatusState.swift in Sources */, D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */, D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */, D61099F12145686D00432DC2 /* List.swift in Sources */, diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index ec4abee2..93a4f146 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -15,8 +15,9 @@ class ConversationTableViewController: EnhancedTableViewController { static let showPostsImage = UIImage(systemName: "eye.fill")! static let hidePostsImage = UIImage(systemName: "eye.slash.fill")! - var mainStatusID: String! - var statusIDs: [String] = [] { + let mainStatusID: String + let mainStatusState: StatusState + var statuses: [(id: String, state: StatusState)] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -27,8 +28,9 @@ class ConversationTableViewController: EnhancedTableViewController { var showStatusesAutomatically = false var visibilityBarButtonItem: UIBarButtonItem! - init(for mainStatusID: String) { + init(for mainStatusID: String, state: StatusState = .unknown) { self.mainStatusID = mainStatusID + self.mainStatusState = state super.init(style: .plain) } @@ -51,9 +53,9 @@ class ConversationTableViewController: EnhancedTableViewController { visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed)) navigationItem.rightBarButtonItem = visibilityBarButtonItem - statusIDs = [mainStatusID] - - guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID!)") } + statuses = [(mainStatusID, mainStatusState)] + + guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") } let request = Status.getContext(mainStatus) MastodonController.client.run(request) { response in @@ -61,8 +63,8 @@ class ConversationTableViewController: EnhancedTableViewController { let parents = self.getDirectParents(of: mainStatus, from: context.ancestors) MastodonCache.addAll(statuses: parents) MastodonCache.addAll(statuses: 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) + 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) } @@ -89,30 +91,30 @@ class ConversationTableViewController: EnhancedTableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return statusIDs.count + return statuses.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let statusID = statusIDs[indexPath.row] + let (id, state) = statuses[indexPath.row] - if statusID == mainStatusID { + if id == mainStatusID { guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() } cell.selectionStyle = .none cell.showStatusAutomatically = showStatusesAutomatically - cell.updateUI(statusID: statusID) + cell.updateUI(statusID: id, state: state) cell.delegate = self return cell } else { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } cell.showStatusAutomatically = showStatusesAutomatically - cell.updateUI(statusID: statusID) + cell.updateUI(statusID: id, state: state) cell.delegate = self return cell } } override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - let statusID = statusIDs[indexPath.row] + let statusID = statuses[indexPath.row].id return statusID == mainStatusID ? nil : indexPath } @@ -136,9 +138,14 @@ class ConversationTableViewController: EnhancedTableViewController { cell.collapsible else { continue } cell.showStatusAutomatically = showStatusesAutomatically cell.setCollapsed(!showStatusesAutomatically, animated: false) + let indexPath = tableView.indexPath(for: cell)! + statuses[indexPath.row].state.collapsed = !showStatusesAutomatically } - statusCollapsedStateChanged() + // recalculate cell heights + tableView.beginUpdates() + tableView.endUpdates() + if showStatusesAutomatically { visibilityBarButtonItem.image = ConversationTableViewController.hidePostsImage } else { @@ -149,7 +156,7 @@ class ConversationTableViewController: EnhancedTableViewController { } extension ConversationTableViewController: StatusTableViewCellDelegate { - func statusCollapsedStateChanged() { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() tableView.endUpdates() @@ -159,7 +166,7 @@ extension ConversationTableViewController: StatusTableViewCellDelegate { extension ConversationTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statusIDs[indexPath.row]) else { continue } + guard let status = MastodonCache.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) @@ -169,7 +176,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statusIDs[indexPath.row]) else { continue } + guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { ImageCache.attachments.cancel(attachment.url) diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 3b85b016..7d420425 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -91,7 +91,7 @@ class NotificationsTableViewController: EnhancedTableViewController { let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - cell.updateUI(statusID: notification.status!.id) + cell.updateUI(statusID: notification.status!.id, state: group.statusState!) cell.delegate = self return cell @@ -212,7 +212,7 @@ class NotificationsTableViewController: EnhancedTableViewController { } extension NotificationsTableViewController: StatusTableViewCellDelegate { - func statusCollapsedStateChanged() { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() tableView.endUpdates() diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 3653521a..1d08c17c 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -22,14 +22,14 @@ class ProfileTableViewController: EnhancedTableViewController { } } - var pinnedStatusIDs: [String] = [] { + var pinnedStatuses: [(id: String, state: StatusState)] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() } } } - var timelineSegments: [TimelineSegment] = [] { + var timelineSegments: [[(id: String, state: StatusState)]] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -109,14 +109,14 @@ class ProfileTableViewController: EnhancedTableViewController { guard case let .success(statuses, _) = response else { fatalError() } MastodonCache.addAll(statuses: statuses) - self.pinnedStatusIDs = statuses.map { $0.id } + self.pinnedStatuses = statuses.map { ($0.id, .unknown) } } getStatuses() { response in guard case let .success(statuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: statuses) - self.timelineSegments.append(TimelineSegment(objects: statuses)) + self.timelineSegments.append(statuses.map { ($0.id, .unknown) }) self.older = pagination?.older self.newer = pagination?.newer @@ -150,7 +150,7 @@ class ProfileTableViewController: EnhancedTableViewController { if section == 0 { return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1 } else if section == 1 { - return pinnedStatusIDs.count + return pinnedStatuses.count } else { return timelineSegments[section - 2].count } @@ -166,15 +166,15 @@ class ProfileTableViewController: EnhancedTableViewController { return cell case 1: guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - let statusID = pinnedStatusIDs[indexPath.row] + let (id, state) = pinnedStatuses[indexPath.row] cell.showPinned = true - cell.updateUI(statusID: statusID) + cell.updateUI(statusID: id, state: state) cell.delegate = self return cell default: guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - let statusID = timelineSegments[indexPath.section - 2][indexPath.row] - cell.updateUI(statusID: statusID) + let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row] + cell.updateUI(statusID: id, state: state) cell.delegate = self return cell } @@ -188,7 +188,7 @@ class ProfileTableViewController: EnhancedTableViewController { guard case let .success(newStatuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: newStatuses) - self.timelineSegments[indexPath.section - 2].append(objects: newStatuses) + self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) self.older = pagination?.older } @@ -214,7 +214,7 @@ class ProfileTableViewController: EnhancedTableViewController { guard case let .success(newStatuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: newStatuses) - self.timelineSegments[0].insertAtBeginning(objects: newStatuses) + self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) self.newer = pagination?.newer @@ -231,7 +231,7 @@ class ProfileTableViewController: EnhancedTableViewController { } extension ProfileTableViewController: StatusTableViewCellDelegate { - func statusCollapsedStateChanged() { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() tableView.endUpdates() @@ -270,7 +270,7 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { 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] + let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id guard let status = MastodonCache.status(for: statusID) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { @@ -281,7 +281,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] + let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id guard let status = MastodonCache.status(for: statusID) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { diff --git a/Tusker/Screens/Search/SearchTableViewController.swift b/Tusker/Screens/Search/SearchTableViewController.swift index 7455149b..1b90c6b5 100644 --- a/Tusker/Screens/Search/SearchTableViewController.swift +++ b/Tusker/Screens/Search/SearchTableViewController.swift @@ -54,9 +54,9 @@ class SearchTableViewController: EnhancedTableViewController { cell.updateUI(hashtag: tag) cell.delegate = self return cell - case let .status(id): + case let .status(id, state): let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell - cell.updateUI(statusID: id) + cell.updateUI(statusID: id, state: state) cell.delegate = self return cell } @@ -113,16 +113,16 @@ class SearchTableViewController: EnhancedTableViewController { var snapshot = NSDiffableDataSourceSnapshot() if !results.accounts.isEmpty { snapshot.appendSections([.accounts]) - snapshot.appendItems(results.accounts.map { Item.account($0.id) }, toSection: .accounts) + snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts) MastodonCache.addAll(accounts: results.accounts) } if !results.hashtags.isEmpty { snapshot.appendSections([.hashtags]) - snapshot.appendItems(results.hashtags.map { Item.hashtag($0) }, toSection: .hashtags) + snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags) } if !results.statuses.isEmpty { snapshot.appendSections([.statuses]) - snapshot.appendItems(results.statuses.map { Item.status($0.id) }, toSection: .statuses) + snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) MastodonCache.addAll(statuses: results.statuses) MastodonCache.addAll(accounts: results.statuses.map { $0.account }) } @@ -152,7 +152,7 @@ extension SearchTableViewController { enum Item: Hashable { case account(String) case hashtag(Hashtag) - case status(String) + case status(String, StatusState) } class DataSource: UITableViewDiffableDataSource { @@ -176,7 +176,7 @@ extension SearchTableViewController: UISearchBarDelegate { } extension SearchTableViewController: StatusTableViewCellDelegate { - func statusCollapsedStateChanged() { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { tableView.beginUpdates() tableView.endUpdates() } diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift index 111430e4..c5ccc6d2 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift @@ -16,6 +16,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { let actionType: ActionType let statusID: String + var statusState: StatusState var accountIDs: [String]? { didSet { tableView.reloadData() @@ -32,9 +33,10 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { - Parameter statusID The ID of the status to show. - Parameter accountIDs The accounts that will be shown. If `nil` is passed, a request will be performed to load the accounts. */ - init(actionType: ActionType, statusID: String, accountIDs: [String]?) { + init(actionType: ActionType, statusID: String, statusState: StatusState, accountIDs: [String]?) { self.actionType = actionType self.statusID = statusID + self.statusState = statusState self.accountIDs = accountIDs super.init(style: .grouped) @@ -109,7 +111,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { switch indexPath.section { case 0: guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - cell.updateUI(statusID: statusID) + cell.updateUI(statusID: statusID, state: statusState) cell.delegate = self return cell case 1: @@ -135,7 +137,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { } extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate { - func statusCollapsedStateChanged() { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() tableView.endUpdates() diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 61a3a476..81df1d64 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -26,7 +26,7 @@ class TimelineTableViewController: EnhancedTableViewController { var timeline: Timeline! - var timelineSegments: [TimelineSegment] = [] { + var timelineSegments: [[(id: String, state: StatusState)]] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -56,7 +56,7 @@ class TimelineTableViewController: EnhancedTableViewController { } func statusID(for indexPath: IndexPath) -> String { - return timelineSegments[indexPath.section][indexPath.row] + return timelineSegments[indexPath.section][indexPath.row].id } override func viewDidLoad() { @@ -74,7 +74,7 @@ class TimelineTableViewController: EnhancedTableViewController { MastodonController.client.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: statuses) - self.timelineSegments.insert(TimelineSegment(objects: statuses), at: 0) + self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.newer = pagination?.newer self.older = pagination?.older } @@ -94,7 +94,8 @@ class TimelineTableViewController: EnhancedTableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - cell.updateUI(statusID: statusID(for: indexPath)) + let (id, state) = timelineSegments[indexPath.section][indexPath.row] + cell.updateUI(statusID: id, state: state) cell.delegate = self return cell @@ -112,7 +113,7 @@ class TimelineTableViewController: EnhancedTableViewController { guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older MastodonCache.addAll(statuses: newStatuses) - self.timelineSegments[self.timelineSegments.count - 1].append(objects: newStatuses) + self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) } } } @@ -137,7 +138,7 @@ class TimelineTableViewController: EnhancedTableViewController { guard case let .success(newStatuses, pagination) = response else { fatalError() } self.newer = pagination?.newer MastodonCache.addAll(statuses: newStatuses) - self.timelineSegments[0].insertAtBeginning(objects: newStatuses) + self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() @@ -154,7 +155,7 @@ class TimelineTableViewController: EnhancedTableViewController { } extension TimelineTableViewController: StatusTableViewCellDelegate { - func statusCollapsedStateChanged() { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() tableView.endUpdates() diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index e7707eba..df4d96c5 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -24,6 +24,8 @@ protocol TuskerNavigationDelegate { func selected(status statusID: String) + func selected(status statusID: String, state: StatusState) + func compose() func reply(to statusID: String) @@ -46,7 +48,7 @@ protocol TuskerNavigationDelegate { func showFollowedByList(accountIDs: [String]) - func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, accountIDs: [String]?) -> StatusActionAccountListTableViewController + func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController } extension TuskerNavigationDelegate where Self: UIViewController { @@ -96,13 +98,18 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func selected(status statusID: String) { + self.selected(status: statusID, state: .unknown) + } + + func selected(status statusID: String, state: StatusState) { + // todo: is this necessary? should the conversation main status cell prevent this // don't open if the conversation is the same as the current one if let conversationController = self as? ConversationTableViewController, conversationController.mainStatusID == statusID { return } - show(ConversationTableViewController(for: statusID), sender: self) + show(ConversationTableViewController(for: statusID, state: state), sender: self) } func compose() { @@ -195,8 +202,8 @@ extension TuskerNavigationDelegate where Self: UIViewController { show(vc, sender: self) } - func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, accountIDs: [String]?) -> StatusActionAccountListTableViewController { - return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, accountIDs: accountIDs) + func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController { + return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs) } } diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index d05e2f67..f6bff913 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -165,7 +165,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { default: fatalError() } - let vc = delegate.statusActionAccountList(action: action, statusID: statusID, accountIDs: accountIDs) + let vc = delegate.statusActionAccountList(action: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs) delegate.show(vc) } } @@ -187,7 +187,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider { default: fatalError() } - return self.delegate?.statusActionAccountList(action: action, statusID: self.statusID, accountIDs: accountIDs) + return self.delegate?.statusActionAccountList(action: action, statusID: self.statusID, statusState: .unknown, accountIDs: accountIDs) }, actions: { return [] }) diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index c81d3274..e9e9cf38 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -11,7 +11,7 @@ import Pachyderm import Combine protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { - func statusCollapsedStateChanged() + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) } class BaseStatusTableViewCell: UITableViewCell { @@ -48,12 +48,18 @@ class BaseStatusTableViewCell: UITableViewCell { } } + var statusState: StatusState! var collapsible = false { didSet { collapseButton.isHidden = !collapsible + statusState?.collapsible = collapsible + } + } + var collapsed = false { + didSet { + statusState?.collapsed = collapsed } } - var collapsed = false var showStatusAutomatically = false var avatarURL: URL? @@ -99,11 +105,12 @@ class BaseStatusTableViewCell: UITableViewCell { .sink(receiveValue: updateUI(account:)) } - func updateUI(statusID: String) { + func updateUI(statusID: String, state: StatusState) { guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status") } self.statusID = statusID + self.statusState = state let account = status.account self.accountID = account.id @@ -119,20 +126,28 @@ class BaseStatusTableViewCell: UITableViewCell { contentLabel.statusID = statusID - collapsible = !status.spoilerText.isEmpty - var shouldCollapse = collapsible - contentWarningLabel.text = status.spoilerText - contentWarningLabel.isHidden = status.spoilerText.isEmpty - if !shouldCollapse, - let text = contentLabel.text, - text.count > 500 { - collapsible = true - shouldCollapse = true + if state.unknown { + collapsible = !status.spoilerText.isEmpty + var shouldCollapse = collapsible + contentWarningLabel.text = status.spoilerText + contentWarningLabel.isHidden = status.spoilerText.isEmpty + if !shouldCollapse, + let text = contentLabel.text, + text.count > 500 { + collapsible = true + shouldCollapse = true + } + if collapsible && showStatusAutomatically { + shouldCollapse = false + } + setCollapsed(shouldCollapse, animated: false) + + state.collapsible = collapsible + state.collapsed = shouldCollapse + } else { + collapsible = state.collapsible! + setCollapsed(state.collapsed!, animated: false) } - if collapsible && showStatusAutomatically { - shouldCollapse = false - } - setCollapsed(shouldCollapse, animated: false) } func updateStatusState(status: Status) { @@ -182,7 +197,7 @@ class BaseStatusTableViewCell: UITableViewCell { @IBAction func collapseButtonPressed() { setCollapsed(!collapsed, animated: true) - delegate?.statusCollapsedStateChanged() + delegate?.statusCellCollapsedStateChanged(self) } func setCollapsed(_ collapsed: Bool, animated: Bool) { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 2e3a62dd..184fdb5a 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -36,8 +36,8 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!] } - override func updateUI(statusID: String) { - super.updateUI(statusID: statusID) + override func updateUI(statusID: String, state: StatusState) { + super.updateUI(statusID: statusID, state: state) guard let status = MastodonCache.status(for: statusID) else { fatalError() } var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) @@ -70,7 +70,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { @IBAction func totalFavoritesPressed() { if let delegate = delegate { // accounts aren't known, pass nil so the VC will load them - let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, accountIDs: nil) + let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil) vc.showInacurateCountWarning = true delegate.show(vc) } @@ -79,7 +79,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { @IBAction func totalReblogsPressed() { if let delegate = delegate { // accounts aren't known, pass nil so the VC will load them - let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, accountIDs: nil) + let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, statusState: statusState.copy(), accountIDs: nil) vc.showInacurateCountWarning = true delegate.show(vc) } diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 2794a308..7425cd46 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -48,7 +48,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { .sink(receiveValue: updateRebloggerLabel(reblogger:)) } - override func updateUI(statusID: String) { + override func updateUI(statusID: String, state: StatusState) { guard var status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } let realStatusID: String @@ -66,7 +66,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { realStatusID = statusID } - super.updateUI(statusID: realStatusID) + super.updateUI(statusID: realStatusID, state: state) updateTimestamp() @@ -123,7 +123,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { super.setSelected(selected, animated: animated) if selected { - delegate?.selected(status: statusID) + delegate?.selected(status: statusID, state: statusState.copy()) } } @@ -134,7 +134,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? { return ( - content: { ConversationTableViewController(for: self.statusID) }, + content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy()) }, actions: { self.actionsForStatus(statusID: self.statusID) } ) }