forked from shadowfacts/Tusker
Finish converting profiles to collection views
This commit is contained in:
parent
2469d285bc
commit
6bb1f3b7dc
|
@ -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>()
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class MyProfileViewController: ProfileViewController {
|
||||
class MyProfileViewController: NewProfileViewController {
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
super.init(accountID: nil, mastodonController: mastodonController)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue