Finish converting profiles to collection views

This commit is contained in:
Shadowfacts 2022-10-28 21:14:46 -04:00
parent 2469d285bc
commit 6bb1f3b7dc
6 changed files with 153 additions and 36 deletions

View File

@ -35,6 +35,9 @@ class MastodonCachePersistentStore: NSPersistentContainer {
return context
}()
// TODO: consider sending managed objects through this to avoid re-fetching things unnecessarily
// would need to audit existing uses to make sure everything happens on the main thread
// and when updating things on the background context would need to switch to main, refetch, and then publish
let statusSubject = PassthroughSubject<String, Never>()
let accountSubject = PassthroughSubject<String, Never>()
let relationshipSubject = PassthroughSubject<String, Never>()

View File

@ -9,7 +9,7 @@
import UIKit
import Pachyderm
class MyProfileViewController: ProfileViewController {
class MyProfileViewController: NewProfileViewController {
init(mastodonController: MastodonController) {
super.init(accountID: nil, mastodonController: mastodonController)

View File

@ -30,15 +30,6 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
}
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
// var headerCell: NewProfileHeaderCollectionViewCell? {
// guard let accountID,
// isViewLoaded,
// let indexPath = dataSource.indexPath(for: .header(accountID)),
// let cell = collectionView.cellForItem(at: indexPath) as? NewProfileHeaderCollectionViewCell else {
// return nil
// }
// return cell
// }
private(set) var headerCell: NewProfileHeaderCollectionViewCell?
init(accountID: String?, kind: Kind, owner: NewProfileViewController) {
@ -60,7 +51,7 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
}
.store(in: &cancellables)
// TODO: refresh key command
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Profile"))
}
required init?(coder: NSCoder) {
@ -75,16 +66,33 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
}
// TODO: item separators
config.itemSeparatorHandler = { [unowned self] indexPath, sectionSeparatorConfiguration in
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
return sectionSeparatorConfiguration
}
var config = sectionSeparatorConfiguration
if item.hideSeparators {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
}
if case .status(_, _, _) = item {
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
}
return config
}
let layout = UICollectionViewCompositionalLayout.list(using: config)
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
// TODO: drag delegate
collectionView.dragDelegate = self
registerTimelineLikeCells()
dataSource = createDataSource()
// TODO: refresh control
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif
}
override func viewDidLoad() {
@ -93,11 +101,6 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
// let headerCell = UICollectionView.CellRegistration<NewProfileHeaderCollectionViewCell, String> { [unowned self] cell, indexPath, item in
// cell.header.delegate = self.profileHeaderDelegate
// cell.header.updateUI(for: item)
// cell.header.pagesSegmentedControl.selectedSegmentIndex = self.owner.currentIndex ?? 0
// }
collectionView.register(NewProfileHeaderCollectionViewCell.self, forCellWithReuseIdentifier: "headerCell")
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, StatusState, Bool)> { [unowned self] cell, indexPath, item in
cell.delegate = self
@ -148,14 +151,6 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// TODO: prune offscreen rows
}
// TODO: refreshing
func setAccountID(_ id: String) {
self.accountID = id
// TODO: maybe this function should be async?
@ -166,7 +161,8 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
private func load() async {
guard accountID != nil,
await controller.state == .notLoadedInitial else {
await controller.state == .notLoadedInitial,
isViewLoaded else {
return
}
@ -213,6 +209,18 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
await apply(snapshot, animatingDifferences: true)
}
@objc func refresh() {
Task {
// TODO: coalesce these data source updates
// TODO: refresh profile
await controller.loadNewer()
await tryLoadPinned()
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl?.endRefreshing()
#endif
}
}
}
extension NewProfileStatusesViewController {
@ -275,6 +283,24 @@ extension NewProfileStatusesViewController {
hasher.combine(3)
}
}
var hideSeparators: Bool {
switch self {
case .loadingIndicator, .confirmLoadMore:
return true
default:
return false
}
}
var isSelectable: Bool {
switch self {
case .status(id: _, state: _, pinned: _):
return true
default:
return false
}
}
}
}
@ -371,7 +397,31 @@ extension NewProfileStatusesViewController: UICollectionViewDelegate {
}
}
// TODO: cell selection
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.isSelectable ?? false
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard case .status(id: let id, state: let state, pinned: _) = dataSource.itemIdentifier(for: indexPath) else {
return
}
let status = mastodonController.persistentContainer.status(for: id)!
selected(status: status.reblog?.id ?? id, state: state.copy())
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
}
extension NewProfileStatusesViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
(collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
}
}
extension NewProfileStatusesViewController: TuskerNavigationDelegate {

View File

@ -8,6 +8,7 @@
import UIKit
import Pachyderm
import Combine
class NewProfileViewController: UIPageViewController {
@ -35,6 +36,8 @@ class NewProfileViewController: UIPageViewController {
private var state: State = .idle
private var cancellables = Set<AnyCancellable>()
init(accountID: String?, mastodonController: MastodonController) {
self.accountID = accountID
self.mastodonController = mastodonController
@ -46,6 +49,12 @@ class NewProfileViewController: UIPageViewController {
.init(accountID: accountID, kind: .withReplies, owner: self),
.init(accountID: accountID, kind: .onlyMedia, owner: self),
]
// try to update the account UI immediately if possible, to avoid the navigation title popping in later
if let accountID,
let account = mastodonController.persistentContainer.account(for: accountID) {
updateAccountUI(account: account)
}
}
required init?(coder: NSCoder) {
@ -63,15 +72,37 @@ class NewProfileViewController: UIPageViewController {
selectPage(at: 0, animated: false)
// TODO: compose button
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
composeButton.menu = UIMenu(children: [
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), handler: { [unowned self] _ in
self.composeDirectMentioning()
})
])
composeButton.isEnabled = mastodonController.loggedIn
navigationItem.rightBarButtonItem = composeButton
// TODO: key commands
addKeyCommand(MenuController.prevSubTabCommand)
addKeyCommand(MenuController.nextSubTabCommand)
mastodonController.persistentContainer.accountSubject
.receive(on: DispatchQueue.main)
.filter { [unowned self] in $0 == self.accountID }
.sink { [unowned self] id in
let account = self.mastodonController.persistentContainer.account(for: id)!
self.updateAccountUI(account: account)
}
.store(in: &cancellables)
Task {
await loadAccount()
}
// TODO: configure nav controller appearance
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
if let nav = navigationController {
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
nav.navigationBar.scrollEdgeAppearance = appearance
}
}
private func loadAccount() async {
@ -221,11 +252,30 @@ class NewProfileViewController: UIPageViewController {
}
}
// MARK: Interaction
@objc private func composeMentioning() {
if let accountID,
let account = mastodonController.persistentContainer.account(for: accountID) {
compose(mentioningAcct: account.acct)
}
}
private func composeDirectMentioning() {
if let accountID,
let account = mastodonController.persistentContainer.account(for: accountID) {
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
draft.visibility = .direct
compose(editing: draft)
}
}
}
extension NewProfileViewController {
enum State {
case idle
case animating
}
}
extension NewProfileViewController: TuskerNavigationDelegate {
@ -243,3 +293,15 @@ extension NewProfileViewController: ProfileHeaderViewDelegate {
selectPage(at: newIndex, animated: true)
}
}
extension NewProfileViewController: TabbedPageViewController {
func selectNextPage() {
guard currentIndex < pageControllers.count - 1 else { return }
selectPage(at: currentIndex + 1, animated: true)
}
func selectPrevPage() {
guard currentIndex > 0 else { return }
selectPage(at: currentIndex - 1, animated: true)
}
}

View File

@ -61,8 +61,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
config.bottomSeparatorVisibility = .hidden
}
if case .status(_, _) = item {
config.topSeparatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
config.bottomSeparatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
}
return config
}
@ -250,7 +250,7 @@ extension TimelineViewController {
var hideSeparators: Bool {
switch self {
case .loadingIndicator, .publicTimelineDescription:
case .loadingIndicator, .publicTimelineDescription, .confirmLoadMore:
return true
default:
return false

View File

@ -12,6 +12,8 @@ import Combine
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
// MARK: Subviews
private lazy var reblogLabel = EmojiLabel().configure {