diff --git a/Pachyderm/Utilities/NotificationGroup.swift b/Pachyderm/Utilities/NotificationGroup.swift index 7907b3c793..b53ea992f0 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 0000000000..c3b380c954 --- /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 1b581a1893..851bd5faf3 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 ec4abee2c0..93a4f14677 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 3b85b0161d..7d420425b1 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 3653521a91..1d08c17c6f 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 7455149b95..1b90c6b5b6 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 111430e47b..c5ccc6d21c 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 61a3a476bf..81df1d64e9 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 e7707eba43..df4d96c532 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 d05e2f6741..f6bff9132b 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 c81d3274d0..e9e9cf3814 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 2e3a62dd73..184fdb5a49 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 2794a308d3..7425cd46bb 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) } ) }