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
This commit is contained in:
Shadowfacts 2019-11-28 18:36:58 -05:00
parent 24a1e7ceb9
commit b47b08fa95
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
14 changed files with 164 additions and 82 deletions

View File

@ -12,12 +12,18 @@ public class NotificationGroup {
public let notificationIDs: [String] public let notificationIDs: [String]
public let id: String public let id: String
public let kind: Notification.Kind public let kind: Notification.Kind
public let statusState: StatusState?
init?(notifications: [Notification]) { init?(notifications: [Notification]) {
guard !notifications.isEmpty else { return nil } guard !notifications.isEmpty else { return nil }
self.notificationIDs = notifications.map { $0.id } self.notificationIDs = notifications.map { $0.id }
self.id = notifications.first!.id self.id = notifications.first!.id
self.kind = notifications.first!.kind 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] { public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {

View File

@ -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
}
}

View File

@ -90,6 +90,7 @@
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.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 */; }; 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 */; }; D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; }; 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 = "<group>"; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; }; D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
D60A548B21ED515800F1F87C /* GMImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GMImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; D60A548D21ED515800F1F87C /* GMImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GMImagePicker.h; sourceTree = "<group>"; };
D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -1000,6 +1002,7 @@
D6A3BC7223218C6E00FD64D5 /* Utilities */ = { D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D60A4FFB238B726A008AC647 /* StatusState.swift */,
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */, D6E6F26221603F8B006A8599 /* CharacterCounter.swift */,
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */, D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */,
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */, D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */,
@ -1534,6 +1537,7 @@
D61099F5214568C300432DC2 /* Notification.swift in Sources */, D61099F5214568C300432DC2 /* Notification.swift in Sources */,
D61099EF214566C000432DC2 /* Instance.swift in Sources */, D61099EF214566C000432DC2 /* Instance.swift in Sources */,
D61099D22144B2E600432DC2 /* Body.swift in Sources */, D61099D22144B2E600432DC2 /* Body.swift in Sources */,
D63569E023908A8D003DD353 /* StatusState.swift in Sources */,
D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */, D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */,
D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */, D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */,
D61099F12145686D00432DC2 /* List.swift in Sources */, D61099F12145686D00432DC2 /* List.swift in Sources */,

View File

@ -15,8 +15,9 @@ class ConversationTableViewController: EnhancedTableViewController {
static let showPostsImage = UIImage(systemName: "eye.fill")! static let showPostsImage = UIImage(systemName: "eye.fill")!
static let hidePostsImage = UIImage(systemName: "eye.slash.fill")! static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
var mainStatusID: String! let mainStatusID: String
var statusIDs: [String] = [] { let mainStatusState: StatusState
var statuses: [(id: String, state: StatusState)] = [] {
didSet { didSet {
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.reloadData() self.tableView.reloadData()
@ -27,8 +28,9 @@ class ConversationTableViewController: EnhancedTableViewController {
var showStatusesAutomatically = false var showStatusesAutomatically = false
var visibilityBarButtonItem: UIBarButtonItem! var visibilityBarButtonItem: UIBarButtonItem!
init(for mainStatusID: String) { init(for mainStatusID: String, state: StatusState = .unknown) {
self.mainStatusID = mainStatusID self.mainStatusID = mainStatusID
self.mainStatusState = state
super.init(style: .plain) super.init(style: .plain)
} }
@ -51,9 +53,9 @@ class ConversationTableViewController: EnhancedTableViewController {
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed)) visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
navigationItem.rightBarButtonItem = visibilityBarButtonItem navigationItem.rightBarButtonItem = visibilityBarButtonItem
statusIDs = [mainStatusID] statuses = [(mainStatusID, mainStatusState)]
guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID!)") } guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") }
let request = Status.getContext(mainStatus) let request = Status.getContext(mainStatus)
MastodonController.client.run(request) { response in MastodonController.client.run(request) { response in
@ -61,8 +63,8 @@ class ConversationTableViewController: EnhancedTableViewController {
let parents = self.getDirectParents(of: mainStatus, from: context.ancestors) let parents = self.getDirectParents(of: mainStatus, from: context.ancestors)
MastodonCache.addAll(statuses: parents) MastodonCache.addAll(statuses: parents)
MastodonCache.addAll(statuses: context.descendants) MastodonCache.addAll(statuses: context.descendants)
self.statusIDs = parents.map { $0.id } + [self.mainStatusID] + context.descendants.map { $0.id } self.statuses = parents.map { ($0.id, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
let indexPath = IndexPath(row: self.statusIDs.firstIndex(of: self.mainStatusID)!, section: 0) let indexPath = IndexPath(row: parents.count, section: 0)
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) 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 { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return statusIDs.count return statuses.count
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 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() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() }
cell.selectionStyle = .none cell.selectionStyle = .none
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.updateUI(statusID: statusID) cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
return cell return cell
} else { } else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.updateUI(statusID: statusID) cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
return cell return cell
} }
} }
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { 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 return statusID == mainStatusID ? nil : indexPath
} }
@ -136,8 +138,13 @@ class ConversationTableViewController: EnhancedTableViewController {
cell.collapsible else { continue } cell.collapsible else { continue }
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.setCollapsed(!showStatusesAutomatically, animated: false) 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 { if showStatusesAutomatically {
visibilityBarButtonItem.image = ConversationTableViewController.hidePostsImage visibilityBarButtonItem.image = ConversationTableViewController.hidePostsImage
@ -149,7 +156,7 @@ class ConversationTableViewController: EnhancedTableViewController {
} }
extension ConversationTableViewController: StatusTableViewCellDelegate { extension ConversationTableViewController: StatusTableViewCellDelegate {
func statusCollapsedStateChanged() { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()
@ -159,7 +166,7 @@ extension ConversationTableViewController: StatusTableViewCellDelegate {
extension ConversationTableViewController: UITableViewDataSourcePrefetching { extension ConversationTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = 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) ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.get(attachment.url, completion: nil) ImageCache.attachments.get(attachment.url, completion: nil)
@ -169,7 +176,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = 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) ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.cancel(attachment.url) ImageCache.attachments.cancel(attachment.url)

View File

@ -91,7 +91,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else {
fatalError() fatalError()
} }
cell.updateUI(statusID: notification.status!.id) cell.updateUI(statusID: notification.status!.id, state: group.statusState!)
cell.delegate = self cell.delegate = self
return cell return cell
@ -212,7 +212,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
} }
extension NotificationsTableViewController: StatusTableViewCellDelegate { extension NotificationsTableViewController: StatusTableViewCellDelegate {
func statusCollapsedStateChanged() { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()

View File

@ -22,14 +22,14 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
} }
var pinnedStatusIDs: [String] = [] { var pinnedStatuses: [(id: String, state: StatusState)] = [] {
didSet { didSet {
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.reloadData() self.tableView.reloadData()
} }
} }
} }
var timelineSegments: [TimelineSegment<Status>] = [] { var timelineSegments: [[(id: String, state: StatusState)]] = [] {
didSet { didSet {
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.reloadData() self.tableView.reloadData()
@ -109,14 +109,14 @@ class ProfileTableViewController: EnhancedTableViewController {
guard case let .success(statuses, _) = response else { fatalError() } guard case let .success(statuses, _) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) MastodonCache.addAll(statuses: statuses)
self.pinnedStatusIDs = statuses.map { $0.id } self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
} }
getStatuses() { response in getStatuses() { response in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) MastodonCache.addAll(statuses: statuses)
self.timelineSegments.append(TimelineSegment(objects: statuses)) self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
self.older = pagination?.older self.older = pagination?.older
self.newer = pagination?.newer self.newer = pagination?.newer
@ -150,7 +150,7 @@ class ProfileTableViewController: EnhancedTableViewController {
if section == 0 { if section == 0 {
return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1 return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1
} else if section == 1 { } else if section == 1 {
return pinnedStatusIDs.count return pinnedStatuses.count
} else { } else {
return timelineSegments[section - 2].count return timelineSegments[section - 2].count
} }
@ -166,15 +166,15 @@ class ProfileTableViewController: EnhancedTableViewController {
return cell return cell
case 1: case 1:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } 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.showPinned = true
cell.updateUI(statusID: statusID) cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
return cell return cell
default: default:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
let statusID = timelineSegments[indexPath.section - 2][indexPath.row] let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row]
cell.updateUI(statusID: statusID) cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
return cell return cell
} }
@ -188,7 +188,7 @@ class ProfileTableViewController: EnhancedTableViewController {
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: newStatuses) 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 self.older = pagination?.older
} }
@ -214,7 +214,7 @@ class ProfileTableViewController: EnhancedTableViewController {
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: newStatuses) 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 self.newer = pagination?.newer
@ -231,7 +231,7 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
extension ProfileTableViewController: StatusTableViewCellDelegate { extension ProfileTableViewController: StatusTableViewCellDelegate {
func statusCollapsedStateChanged() { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()
@ -270,7 +270,7 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
extension ProfileTableViewController: UITableViewDataSourcePrefetching { extension ProfileTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths where indexPath.section > 1 { 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 } guard let status = MastodonCache.status(for: statusID) else { continue }
ImageCache.avatars.get(status.account.avatar, completion: nil) ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
@ -281,7 +281,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths where indexPath.section > 1 { 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 } guard let status = MastodonCache.status(for: statusID) else { continue }
ImageCache.avatars.cancel(status.account.avatar) ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {

View File

@ -54,9 +54,9 @@ class SearchTableViewController: EnhancedTableViewController {
cell.updateUI(hashtag: tag) cell.updateUI(hashtag: tag)
cell.delegate = self cell.delegate = self
return cell return cell
case let .status(id): case let .status(id, state):
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell
cell.updateUI(statusID: id) cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
return cell return cell
} }
@ -113,16 +113,16 @@ class SearchTableViewController: EnhancedTableViewController {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
if !results.accounts.isEmpty { if !results.accounts.isEmpty {
snapshot.appendSections([.accounts]) 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) MastodonCache.addAll(accounts: results.accounts)
} }
if !results.hashtags.isEmpty { if !results.hashtags.isEmpty {
snapshot.appendSections([.hashtags]) 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 { if !results.statuses.isEmpty {
snapshot.appendSections([.statuses]) 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(statuses: results.statuses)
MastodonCache.addAll(accounts: results.statuses.map { $0.account }) MastodonCache.addAll(accounts: results.statuses.map { $0.account })
} }
@ -152,7 +152,7 @@ extension SearchTableViewController {
enum Item: Hashable { enum Item: Hashable {
case account(String) case account(String)
case hashtag(Hashtag) case hashtag(Hashtag)
case status(String) case status(String, StatusState)
} }
class DataSource: UITableViewDiffableDataSource<Section, Item> { class DataSource: UITableViewDiffableDataSource<Section, Item> {
@ -176,7 +176,7 @@ extension SearchTableViewController: UISearchBarDelegate {
} }
extension SearchTableViewController: StatusTableViewCellDelegate { extension SearchTableViewController: StatusTableViewCellDelegate {
func statusCollapsedStateChanged() { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()
} }

View File

@ -16,6 +16,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
let actionType: ActionType let actionType: ActionType
let statusID: String let statusID: String
var statusState: StatusState
var accountIDs: [String]? { var accountIDs: [String]? {
didSet { didSet {
tableView.reloadData() tableView.reloadData()
@ -32,9 +33,10 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
- Parameter statusID The ID of the status to show. - 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. - 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.actionType = actionType
self.statusID = statusID self.statusID = statusID
self.statusState = statusState
self.accountIDs = accountIDs self.accountIDs = accountIDs
super.init(style: .grouped) super.init(style: .grouped)
@ -109,7 +111,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
switch indexPath.section { switch indexPath.section {
case 0: case 0:
guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } 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 cell.delegate = self
return cell return cell
case 1: case 1:
@ -135,7 +137,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
} }
extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate { extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate {
func statusCollapsedStateChanged() { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()

View File

@ -26,7 +26,7 @@ class TimelineTableViewController: EnhancedTableViewController {
var timeline: Timeline! var timeline: Timeline!
var timelineSegments: [TimelineSegment<Status>] = [] { var timelineSegments: [[(id: String, state: StatusState)]] = [] {
didSet { didSet {
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.reloadData() self.tableView.reloadData()
@ -56,7 +56,7 @@ class TimelineTableViewController: EnhancedTableViewController {
} }
func statusID(for indexPath: IndexPath) -> String { func statusID(for indexPath: IndexPath) -> String {
return timelineSegments[indexPath.section][indexPath.row] return timelineSegments[indexPath.section][indexPath.row].id
} }
override func viewDidLoad() { override func viewDidLoad() {
@ -74,7 +74,7 @@ class TimelineTableViewController: EnhancedTableViewController {
MastodonController.client.run(request) { response in MastodonController.client.run(request) { response in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) 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.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
} }
@ -94,7 +94,8 @@ class TimelineTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } 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 cell.delegate = self
return cell return cell
@ -112,7 +113,7 @@ class TimelineTableViewController: EnhancedTableViewController {
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.older = pagination?.older self.older = pagination?.older
MastodonCache.addAll(statuses: newStatuses) 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() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.newer = pagination?.newer self.newer = pagination?.newer
MastodonCache.addAll(statuses: newStatuses) 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 { DispatchQueue.main.async {
self.refreshControl?.endRefreshing() self.refreshControl?.endRefreshing()
@ -154,7 +155,7 @@ class TimelineTableViewController: EnhancedTableViewController {
} }
extension TimelineTableViewController: StatusTableViewCellDelegate { extension TimelineTableViewController: StatusTableViewCellDelegate {
func statusCollapsedStateChanged() { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()

View File

@ -24,6 +24,8 @@ protocol TuskerNavigationDelegate {
func selected(status statusID: String) func selected(status statusID: String)
func selected(status statusID: String, state: StatusState)
func compose() func compose()
func reply(to statusID: String) func reply(to statusID: String)
@ -46,7 +48,7 @@ protocol TuskerNavigationDelegate {
func showFollowedByList(accountIDs: [String]) 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 { extension TuskerNavigationDelegate where Self: UIViewController {
@ -96,13 +98,18 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
func selected(status statusID: String) { 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 // don't open if the conversation is the same as the current one
if let conversationController = self as? ConversationTableViewController, if let conversationController = self as? ConversationTableViewController,
conversationController.mainStatusID == statusID { conversationController.mainStatusID == statusID {
return return
} }
show(ConversationTableViewController(for: statusID), sender: self) show(ConversationTableViewController(for: statusID, state: state), sender: self)
} }
func compose() { func compose() {
@ -195,8 +202,8 @@ extension TuskerNavigationDelegate where Self: UIViewController {
show(vc, sender: self) show(vc, sender: self)
} }
func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, accountIDs: [String]?) -> StatusActionAccountListTableViewController { func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController {
return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, accountIDs: accountIDs) return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs)
} }
} }

View File

@ -165,7 +165,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
default: default:
fatalError() 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) delegate.show(vc)
} }
} }
@ -187,7 +187,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
default: default:
fatalError() 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: { }, actions: {
return [] return []
}) })

View File

@ -11,7 +11,7 @@ import Pachyderm
import Combine import Combine
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
func statusCollapsedStateChanged() func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
} }
class BaseStatusTableViewCell: UITableViewCell { class BaseStatusTableViewCell: UITableViewCell {
@ -48,12 +48,18 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
} }
var statusState: StatusState!
var collapsible = false { var collapsible = false {
didSet { didSet {
collapseButton.isHidden = !collapsible collapseButton.isHidden = !collapsible
statusState?.collapsible = collapsible
}
}
var collapsed = false {
didSet {
statusState?.collapsed = collapsed
} }
} }
var collapsed = false
var showStatusAutomatically = false var showStatusAutomatically = false
var avatarURL: URL? var avatarURL: URL?
@ -99,11 +105,12 @@ class BaseStatusTableViewCell: UITableViewCell {
.sink(receiveValue: updateUI(account:)) .sink(receiveValue: updateUI(account:))
} }
func updateUI(statusID: String) { func updateUI(statusID: String, state: StatusState) {
guard let status = MastodonCache.status(for: statusID) else { guard let status = MastodonCache.status(for: statusID) else {
fatalError("Missing cached status") fatalError("Missing cached status")
} }
self.statusID = statusID self.statusID = statusID
self.statusState = state
let account = status.account let account = status.account
self.accountID = account.id self.accountID = account.id
@ -119,6 +126,7 @@ class BaseStatusTableViewCell: UITableViewCell {
contentLabel.statusID = statusID contentLabel.statusID = statusID
if state.unknown {
collapsible = !status.spoilerText.isEmpty collapsible = !status.spoilerText.isEmpty
var shouldCollapse = collapsible var shouldCollapse = collapsible
contentWarningLabel.text = status.spoilerText contentWarningLabel.text = status.spoilerText
@ -133,6 +141,13 @@ class BaseStatusTableViewCell: UITableViewCell {
shouldCollapse = false shouldCollapse = false
} }
setCollapsed(shouldCollapse, animated: false) setCollapsed(shouldCollapse, animated: false)
state.collapsible = collapsible
state.collapsed = shouldCollapse
} else {
collapsible = state.collapsible!
setCollapsed(state.collapsed!, animated: false)
}
} }
func updateStatusState(status: Status) { func updateStatusState(status: Status) {
@ -182,7 +197,7 @@ class BaseStatusTableViewCell: UITableViewCell {
@IBAction func collapseButtonPressed() { @IBAction func collapseButtonPressed() {
setCollapsed(!collapsed, animated: true) setCollapsed(!collapsed, animated: true)
delegate?.statusCollapsedStateChanged() delegate?.statusCellCollapsedStateChanged(self)
} }
func setCollapsed(_ collapsed: Bool, animated: Bool) { func setCollapsed(_ collapsed: Bool, animated: Bool) {

View File

@ -36,8 +36,8 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!] accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
} }
override func updateUI(statusID: String) { override func updateUI(statusID: String, state: StatusState) {
super.updateUI(statusID: statusID) super.updateUI(statusID: statusID, state: state)
guard let status = MastodonCache.status(for: statusID) else { fatalError() } guard let status = MastodonCache.status(for: statusID) else { fatalError() }
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
@ -70,7 +70,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
@IBAction func totalFavoritesPressed() { @IBAction func totalFavoritesPressed() {
if let delegate = delegate { if let delegate = delegate {
// accounts aren't known, pass nil so the VC will load them // 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 vc.showInacurateCountWarning = true
delegate.show(vc) delegate.show(vc)
} }
@ -79,7 +79,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
@IBAction func totalReblogsPressed() { @IBAction func totalReblogsPressed() {
if let delegate = delegate { if let delegate = delegate {
// accounts aren't known, pass nil so the VC will load them // 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 vc.showInacurateCountWarning = true
delegate.show(vc) delegate.show(vc)
} }

View File

@ -48,7 +48,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
.sink(receiveValue: updateRebloggerLabel(reblogger:)) .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)") } guard var status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String let realStatusID: String
@ -66,7 +66,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
realStatusID = statusID realStatusID = statusID
} }
super.updateUI(statusID: realStatusID) super.updateUI(statusID: realStatusID, state: state)
updateTimestamp() updateTimestamp()
@ -123,7 +123,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
super.setSelected(selected, animated: animated) super.setSelected(selected, animated: animated)
if selected { 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? { override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
return ( return (
content: { ConversationTableViewController(for: self.statusID) }, content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy()) },
actions: { self.actionsForStatus(statusID: self.statusID) } actions: { self.actionsForStatus(statusID: self.statusID) }
) )
} }