Show threads on Conversation screen
This commit is contained in:
parent
122cce3bc7
commit
bcc023a127
|
@ -272,6 +272,8 @@
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
||||||
|
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */; };
|
||||||
|
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */; };
|
||||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
||||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
||||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */; };
|
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */; };
|
||||||
|
@ -634,6 +636,8 @@
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
||||||
|
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ExpandThreadTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = "<group>"; };
|
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1043,6 +1047,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
||||||
|
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */,
|
||||||
|
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */,
|
||||||
);
|
);
|
||||||
path = Conversation;
|
path = Conversation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1746,6 +1752,7 @@
|
||||||
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
||||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||||
|
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||||
|
@ -2013,6 +2020,7 @@
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
|
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||||
|
|
|
@ -8,23 +8,30 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
class ConversationNode {
|
||||||
|
let status: StatusMO
|
||||||
|
var children: [ConversationNode]
|
||||||
|
|
||||||
|
init(status: StatusMO) {
|
||||||
|
self.status = status
|
||||||
|
self.children = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ConversationTableViewController: EnhancedTableViewController {
|
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")!
|
||||||
|
|
||||||
|
static let bottomSeparatorTag = 101
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
let mainStatusID: String
|
let mainStatusID: String
|
||||||
let mainStatusState: StatusState
|
let mainStatusState: StatusState
|
||||||
var statuses: [(id: String, state: StatusState)] = [] {
|
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
didSet {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
var visibilityBarButtonItem: UIBarButtonItem!
|
var visibilityBarButtonItem: UIBarButtonItem!
|
||||||
|
@ -45,7 +52,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
||||||
for (id, _) in statuses {
|
let snapshot = dataSource.snapshot()
|
||||||
|
for case let .status(id: id, state: _) in snapshot.itemIdentifiers {
|
||||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,13 +68,72 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||||
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
|
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
|
||||||
|
tableView.register(UINib(nibName: "ExpandThreadTableViewCell", bundle: .main), forCellReuseIdentifier: "expandThreadCell")
|
||||||
|
|
||||||
tableView.prefetchDataSource = self
|
tableView.prefetchDataSource = self
|
||||||
|
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
tableView.backgroundColor = .secondarySystemBackground
|
||||||
|
// separators are disabled on the table view so we can re-add them ourselves
|
||||||
|
// so they're not inserted in between statuses in the ame sub-thread
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
|
||||||
|
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
|
switch item {
|
||||||
|
case let .status(id: id, state: state):
|
||||||
|
let rowsInSection = self.dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section)
|
||||||
|
let firstInSection = indexPath.row == 0
|
||||||
|
let lastInSection = indexPath.row == rowsInSection - 1
|
||||||
|
|
||||||
|
let identifier = id == self.mainStatusID ? "mainStatusCell" : "statusCell"
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! BaseStatusTableViewCell
|
||||||
|
|
||||||
|
cell.delegate = self
|
||||||
|
cell.showStatusAutomatically = self.showStatusesAutomatically
|
||||||
|
|
||||||
|
if let cell = cell as? TimelineStatusTableViewCell {
|
||||||
|
cell.showReplyIndicator = false
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
|
||||||
|
cell.setShowThreadLinks(prev: !firstInSection, next: !lastInSection)
|
||||||
|
if lastInSection {
|
||||||
|
if cell.viewWithTag(ConversationTableViewController.bottomSeparatorTag) == nil {
|
||||||
|
let separator = UIView()
|
||||||
|
separator.tag = ConversationTableViewController.bottomSeparatorTag
|
||||||
|
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
separator.backgroundColor = tableView.separatorColor
|
||||||
|
cell.addSubview(separator)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||||
|
separator.bottomAnchor.constraint(equalTo: cell.bottomAnchor),
|
||||||
|
separator.leftAnchor.constraint(equalTo: cell.leftAnchor, constant: cell.separatorInset.left),
|
||||||
|
separator.rightAnchor.constraint(equalTo: cell.rightAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cell.viewWithTag(ConversationTableViewController.bottomSeparatorTag)?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
|
||||||
|
case let .expandThread(childThreads: childThreads):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
||||||
|
cell.updateUI(childThreads: childThreads)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
||||||
|
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||||
|
|
||||||
statuses = [(mainStatusID, mainStatusState)]
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.statuses])
|
||||||
|
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
||||||
fatalError("Missing cached status \(self.mainStatusID)")
|
fatalError("Missing cached status \(self.mainStatusID)")
|
||||||
|
@ -80,19 +147,39 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||||
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses) {
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: context.descendants) {
|
// todo: should this really be blindly adding all the descendants?
|
||||||
self.statuses = parents.map { ($0, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
|
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
||||||
let indexPath = IndexPath(row: parents.count, section: 0)
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
var snapshot = self.dataSource.snapshot()
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
snapshot.insertItems(parents.map { .status(id: $0, state: .unknown) }, beforeItem: mainStatusItem)
|
||||||
|
|
||||||
|
// fetch all descendant status managed objects
|
||||||
|
let descendantIDs = context.descendants.map(\.id)
|
||||||
|
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
||||||
|
request.predicate = NSPredicate(format: "id in %@", descendantIDs)
|
||||||
|
|
||||||
|
if let descendants = try? self.mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||||
|
// convert array of descendant statuses into tree of sub-threads
|
||||||
|
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||||
|
|
||||||
|
// convert sub-threads into items for section and add to snapshot
|
||||||
|
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
|
// ensure that the main status is on-screen after newly loaded statuses are added
|
||||||
|
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
||||||
|
if let indexPath = self.dataSource.indexPath(for: mainStatusItem) {
|
||||||
|
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||||
var statuses = statuses
|
var statuses = statuses
|
||||||
var parents = [String]()
|
var parents = [String]()
|
||||||
|
|
||||||
|
@ -107,38 +194,99 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
return parents
|
return parents
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Table view data source
|
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||||
|
var descendants = descendants
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||||
return 1
|
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||||
|
descendants.removeAll { $0.inReplyToID == id }
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes: [String: ConversationNode] = [
|
||||||
|
mainStatus.id: ConversationNode(status: mainStatus)
|
||||||
|
]
|
||||||
|
|
||||||
|
var idsToCheck = [mainStatusID]
|
||||||
|
|
||||||
|
while !idsToCheck.isEmpty {
|
||||||
|
let inReplyToID = idsToCheck.removeFirst()
|
||||||
|
let nodeForID = nodes[inReplyToID]!
|
||||||
|
|
||||||
|
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||||
|
for reply in inReply {
|
||||||
|
idsToCheck.append(reply.id)
|
||||||
|
|
||||||
|
let replyNode = ConversationNode(status: reply)
|
||||||
|
nodes[reply.id] = replyNode
|
||||||
|
|
||||||
|
nodeForID.children.append(replyNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes[mainStatusID]!.children
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||||
return statuses.count
|
var childThreads = childThreads
|
||||||
|
|
||||||
|
// child threads by the same author as the main status come first
|
||||||
|
let pivotIndex = childThreads.partition(by: { $0.status.account.id != mainStatus.account.id })
|
||||||
|
|
||||||
|
// within each group, child threads are sorted chronologically
|
||||||
|
childThreads[0..<pivotIndex].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||||
|
childThreads[pivotIndex...].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||||
|
|
||||||
|
for node in childThreads {
|
||||||
|
snapshot.appendSections([.childThread(firstStatusID: node.status.id)])
|
||||||
|
snapshot.appendItems([.status(id: node.status.id, state: .unknown)])
|
||||||
|
|
||||||
|
var currentNode = node
|
||||||
|
while true {
|
||||||
|
let next: ConversationNode
|
||||||
|
|
||||||
|
if currentNode.children.count == 0 {
|
||||||
|
break
|
||||||
|
} else if currentNode.children.count == 1 {
|
||||||
|
next = currentNode.children[0]
|
||||||
|
} else {
|
||||||
|
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
||||||
|
if sameAuthorStatuses.count == 1 {
|
||||||
|
next = sameAuthorStatuses[0]
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems([.expandThread(childThreads: currentNode.children)])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = next
|
||||||
|
snapshot.appendItems([.status(id: next.status.id, state: .unknown)])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
func item(for indexPath: IndexPath) -> (id: String, state: StatusState)? {
|
||||||
let (id, state) = statuses[indexPath.row]
|
return self.dataSource.itemIdentifier(for: indexPath).flatMap { (item) in
|
||||||
|
switch item {
|
||||||
if id == mainStatusID {
|
case let .status(id: id, state: state):
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() }
|
return (id: id, state: state)
|
||||||
cell.selectionStyle = .none
|
default:
|
||||||
cell.showStatusAutomatically = showStatusesAutomatically
|
return nil
|
||||||
cell.delegate = self
|
}
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
return cell
|
|
||||||
} else {
|
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
|
||||||
cell.showStatusAutomatically = showStatusesAutomatically
|
|
||||||
cell.showReplyIndicator = false
|
|
||||||
cell.delegate = self
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
return cell
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
if case .expandThread = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
||||||
|
self.selected(status: id, state: state)
|
||||||
|
} else {
|
||||||
|
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -154,7 +302,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
@objc func toggleVisibilityButtonPressed() {
|
@objc func toggleVisibilityButtonPressed() {
|
||||||
showStatusesAutomatically = !showStatusesAutomatically
|
showStatusesAutomatically = !showStatusesAutomatically
|
||||||
|
|
||||||
for (_, state) in statuses where state.collapsible == true {
|
let snapshot = dataSource.snapshot()
|
||||||
|
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||||
state.collapsed = !showStatusesAutomatically
|
state.collapsed = !showStatusesAutomatically
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +327,48 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConversationTableViewController {
|
||||||
|
enum Section: Hashable {
|
||||||
|
case statuses
|
||||||
|
case childThread(firstStatusID: String)
|
||||||
|
}
|
||||||
|
enum Item: Hashable {
|
||||||
|
case status(id: String, state: StatusState)
|
||||||
|
case expandThread(childThreads: [ConversationNode])
|
||||||
|
|
||||||
|
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||||
|
return a == b
|
||||||
|
case let (.expandThread(childThreads: a), .expandThread(childThreads: b)):
|
||||||
|
return zip(a, b).allSatisfy { $0.status.id == $1.status.id }
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case let .status(id: id, state: _):
|
||||||
|
hasher.combine("status")
|
||||||
|
hasher.combine(id)
|
||||||
|
case let .expandThread(childThreads: children):
|
||||||
|
hasher.combine("expandThread")
|
||||||
|
hasher.combine(children.map(\.status.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||||
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
||||||
|
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||||
|
// transfer show statuses automatically state when showing new conversation
|
||||||
|
vc.showStatusesAutomatically = self.showStatusesAutomatically
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
@ -189,12 +380,12 @@ extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
extension ConversationTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
extension ConversationTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids = indexPaths.map { statuses[$0.row].id }
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids: [String] = indexPaths.compactMap { statuses[$0.row].id }
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
cancelPrefetchingStatuses(with: ids)
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
//
|
||||||
|
// ExpandThreadTableViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/30/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ExpandThreadTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
@IBOutlet weak var avatarContainerView: UIView!
|
||||||
|
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
||||||
|
@IBOutlet weak var replyCountLabel: UILabel!
|
||||||
|
var avatarImageViews: [UIImageView] = []
|
||||||
|
|
||||||
|
private var avatarRequests: [ImageCache.Request] = []
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
let prevThreadLinkView = UIView()
|
||||||
|
prevThreadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
prevThreadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||||
|
prevThreadLinkView.layer.cornerRadius = 2.5
|
||||||
|
prevThreadLinkView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
contentView.addSubview(prevThreadLinkView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
prevThreadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
||||||
|
prevThreadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 16 + 25),
|
||||||
|
prevThreadLinkView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
prevThreadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2),
|
||||||
|
])
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(childThreads: [ConversationNode]) {
|
||||||
|
let format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
|
||||||
|
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
|
||||||
|
|
||||||
|
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
||||||
|
|
||||||
|
avatarImageViews.forEach { $0.removeFromSuperview() }
|
||||||
|
avatarImageViews = []
|
||||||
|
|
||||||
|
avatarRequests = []
|
||||||
|
|
||||||
|
let avatarImageSize: CGFloat = 44 - 12
|
||||||
|
|
||||||
|
if accounts.count == 1 {
|
||||||
|
avatarContainerWidthConstraint.constant = avatarImageSize
|
||||||
|
} else {
|
||||||
|
avatarContainerWidthConstraint.constant = CGFloat(accounts.count) * avatarImageSize * 3 / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, account) in accounts.enumerated() {
|
||||||
|
let accountImageView = UIImageView()
|
||||||
|
accountImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
accountImageView.contentMode = .scaleAspectFit
|
||||||
|
accountImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
|
||||||
|
accountImageView.layer.masksToBounds = true
|
||||||
|
accountImageView.layer.borderWidth = 1
|
||||||
|
accountImageView.layer.borderColor = UIColor.secondarySystemBackground.cgColor
|
||||||
|
// need a solid background color so semi-transparent avatars don't look bad
|
||||||
|
accountImageView.backgroundColor = .secondarySystemBackground
|
||||||
|
avatarContainerView.addSubview(accountImageView)
|
||||||
|
|
||||||
|
avatarImageViews.append(accountImageView)
|
||||||
|
|
||||||
|
accountImageView.layer.zPosition = CGFloat(-index)
|
||||||
|
|
||||||
|
let xConstraint: NSLayoutConstraint
|
||||||
|
if index == 0 {
|
||||||
|
xConstraint = accountImageView.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor)
|
||||||
|
} else if index == accounts.count - 1 {
|
||||||
|
xConstraint = accountImageView.trailingAnchor.constraint(equalTo: avatarContainerView.trailingAnchor)
|
||||||
|
} else {
|
||||||
|
xConstraint = accountImageView.centerXAnchor.constraint(equalTo: avatarContainerView.centerXAnchor)
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
accountImageView.widthAnchor.constraint(equalToConstant: avatarImageSize),
|
||||||
|
accountImageView.heightAnchor.constraint(equalToConstant: avatarImageSize),
|
||||||
|
accountImageView.centerYAnchor.constraint(equalTo: avatarContainerView.centerYAnchor),
|
||||||
|
xConstraint
|
||||||
|
])
|
||||||
|
|
||||||
|
let req = ImageCache.avatars.get(account.avatar) { [weak accountImageView] (_, image) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
accountImageView?.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let req = req {
|
||||||
|
avatarRequests.append(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
avatarImageViews.forEach {
|
||||||
|
$0.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
avatarRequests.forEach { $0.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="ExpandThreadTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="IXi-sc-YIw">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="173" height="44"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eFB-F1-d3A">
|
||||||
|
<rect key="frame" x="0.0" y="6" width="100" height="32"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="32" id="g9U-u7-718"/>
|
||||||
|
<constraint firstAttribute="width" constant="100" id="tiI-Rj-gjh"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2 replies" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dcm-ll-GeE">
|
||||||
|
<rect key="frame" x="108" y="12" width="65" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="systemBlueColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="44" id="jk2-uV-FdO"/>
|
||||||
|
</constraints>
|
||||||
|
</stackView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kkt-hM-ScW">
|
||||||
|
<rect key="frame" x="16" y="43.5" width="304" height="0.5"/>
|
||||||
|
<color key="backgroundColor" systemColor="separatorColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="0.5" id="Fkq-bT-IYv"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Kkt-hM-ScW" secondAttribute="bottom" id="AvY-H1-0YN"/>
|
||||||
|
<constraint firstItem="Kkt-hM-ScW" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="E5g-hz-SLI"/>
|
||||||
|
<constraint firstItem="IXi-sc-YIw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="SRF-Zx-Y0R"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Kkt-hM-ScW" secondAttribute="trailing" id="YML-R1-ezq"/>
|
||||||
|
<constraint firstItem="IXi-sc-YIw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="iD5-Av-ORS"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="IXi-sc-YIw" secondAttribute="bottom" id="kpD-6Q-qKi"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
|
||||||
|
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
||||||
|
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="132" y="132"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="secondarySystemBackgroundColor">
|
||||||
|
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="separatorColor">
|
||||||
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="systemBlueColor">
|
||||||
|
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -12,6 +12,8 @@ import Pachyderm
|
||||||
|
|
||||||
protocol TuskerNavigationDelegate: UIViewController {
|
protocol TuskerNavigationDelegate: UIViewController {
|
||||||
var apiController: MastodonController { get }
|
var apiController: MastodonController { get }
|
||||||
|
|
||||||
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TuskerNavigationDelegate {
|
extension TuskerNavigationDelegate {
|
||||||
|
@ -64,19 +66,16 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
||||||
|
return ConversationTableViewController(for: mainStatusID, state: state, mastodonController: apiController)
|
||||||
|
}
|
||||||
|
|
||||||
func selected(status statusID: String) {
|
func selected(status statusID: String) {
|
||||||
self.selected(status: statusID, state: .unknown)
|
self.selected(status: statusID, state: .unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selected(status statusID: String, state: StatusState) {
|
func selected(status statusID: String, state: StatusState) {
|
||||||
// todo: is this necessary? should the conversation main status cell prevent this
|
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
||||||
// 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, state: state, mastodonController: apiController), sender: self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
func compose(editing draft: Draft) {
|
||||||
|
|
|
@ -38,6 +38,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var favoriteButton: UIButton!
|
@IBOutlet weak var favoriteButton: UIButton!
|
||||||
@IBOutlet weak var reblogButton: UIButton!
|
@IBOutlet weak var reblogButton: UIButton!
|
||||||
@IBOutlet weak var moreButton: UIButton!
|
@IBOutlet weak var moreButton: UIButton!
|
||||||
|
private(set) var prevThreadLinkView: UIView?
|
||||||
|
private(set) var nextThreadLinkView: UIView?
|
||||||
|
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
@ -278,6 +280,52 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setShowThreadLinks(prev: Bool, next: Bool) {
|
||||||
|
if prev {
|
||||||
|
if let prevThreadLinkView = prevThreadLinkView {
|
||||||
|
prevThreadLinkView.isHidden = false
|
||||||
|
} else {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||||
|
view.layer.cornerRadius = 2.5
|
||||||
|
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
prevThreadLinkView = view
|
||||||
|
addSubview(view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.widthAnchor.constraint(equalToConstant: 5),
|
||||||
|
view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||||
|
view.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prevThreadLinkView?.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if next {
|
||||||
|
if let nextThreadLinkView = nextThreadLinkView {
|
||||||
|
nextThreadLinkView.isHidden = false
|
||||||
|
} else {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||||
|
view.layer.cornerRadius = 2.5
|
||||||
|
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
nextThreadLinkView = view
|
||||||
|
addSubview(view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.widthAnchor.constraint(equalToConstant: 5),
|
||||||
|
view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||||
|
view.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 2),
|
||||||
|
view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextThreadLinkView?.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
|
|
@ -29,5 +29,21 @@
|
||||||
<string>%u people</string>
|
<string>%u people</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>expand threads count</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@replies@</string>
|
||||||
|
<key>replies</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>u</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>1 reply</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>%u replies</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
Loading…
Reference in New Issue