Finish converting profiles to collection views
This commit is contained in:
parent
2469d285bc
commit
6bb1f3b7dc
|
@ -35,6 +35,9 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
return context
|
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 statusSubject = PassthroughSubject<String, Never>()
|
||||||
let accountSubject = PassthroughSubject<String, Never>()
|
let accountSubject = PassthroughSubject<String, Never>()
|
||||||
let relationshipSubject = PassthroughSubject<String, Never>()
|
let relationshipSubject = PassthroughSubject<String, Never>()
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class MyProfileViewController: ProfileViewController {
|
class MyProfileViewController: NewProfileViewController {
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
super.init(accountID: nil, mastodonController: mastodonController)
|
super.init(accountID: nil, mastodonController: mastodonController)
|
||||||
|
|
|
@ -30,15 +30,6 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
|
||||||
}
|
}
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
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?
|
private(set) var headerCell: NewProfileHeaderCollectionViewCell?
|
||||||
|
|
||||||
init(accountID: String?, kind: Kind, owner: NewProfileViewController) {
|
init(accountID: String?, kind: Kind, owner: NewProfileViewController) {
|
||||||
|
@ -60,7 +51,7 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// TODO: refresh key command
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Profile"))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -75,16 +66,33 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
|
||||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
(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)
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
// TODO: drag delegate
|
collectionView.dragDelegate = self
|
||||||
|
|
||||||
registerTimelineLikeCells()
|
registerTimelineLikeCells()
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
|
||||||
// TODO: refresh control
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
|
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
@ -93,11 +101,6 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
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")
|
collectionView.register(NewProfileHeaderCollectionViewCell.self, forCellWithReuseIdentifier: "headerCell")
|
||||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, StatusState, Bool)> { [unowned self] cell, indexPath, item in
|
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, StatusState, Bool)> { [unowned self] cell, indexPath, item in
|
||||||
cell.delegate = self
|
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) {
|
func setAccountID(_ id: String) {
|
||||||
self.accountID = id
|
self.accountID = id
|
||||||
// TODO: maybe this function should be async?
|
// TODO: maybe this function should be async?
|
||||||
|
@ -166,7 +161,8 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
|
||||||
|
|
||||||
private func load() async {
|
private func load() async {
|
||||||
guard accountID != nil,
|
guard accountID != nil,
|
||||||
await controller.state == .notLoadedInitial else {
|
await controller.state == .notLoadedInitial,
|
||||||
|
isViewLoaded else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,6 +209,18 @@ class NewProfileStatusesViewController: UIViewController, TimelineLikeCollection
|
||||||
await apply(snapshot, animatingDifferences: true)
|
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 {
|
extension NewProfileStatusesViewController {
|
||||||
|
@ -275,6 +283,24 @@ extension NewProfileStatusesViewController {
|
||||||
hasher.combine(3)
|
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 {
|
extension NewProfileStatusesViewController: TuskerNavigationDelegate {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import Combine
|
||||||
|
|
||||||
class NewProfileViewController: UIPageViewController {
|
class NewProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ class NewProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
private var state: State = .idle
|
private var state: State = .idle
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(accountID: String?, mastodonController: MastodonController) {
|
init(accountID: String?, mastodonController: MastodonController) {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -46,6 +49,12 @@ class NewProfileViewController: UIPageViewController {
|
||||||
.init(accountID: accountID, kind: .withReplies, owner: self),
|
.init(accountID: accountID, kind: .withReplies, owner: self),
|
||||||
.init(accountID: accountID, kind: .onlyMedia, 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) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -63,15 +72,37 @@ class NewProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
selectPage(at: 0, animated: false)
|
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 {
|
Task {
|
||||||
await loadAccount()
|
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 {
|
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 {
|
enum State {
|
||||||
case idle
|
case idle
|
||||||
case animating
|
case animating
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NewProfileViewController: TuskerNavigationDelegate {
|
extension NewProfileViewController: TuskerNavigationDelegate {
|
||||||
|
@ -243,3 +293,15 @@ extension NewProfileViewController: ProfileHeaderViewDelegate {
|
||||||
selectPage(at: newIndex, animated: true)
|
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
|
config.bottomSeparatorVisibility = .hidden
|
||||||
}
|
}
|
||||||
if case .status(_, _) = item {
|
if case .status(_, _) = item {
|
||||||
config.topSeparatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
config.bottomSeparatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
@ -250,7 +250,7 @@ extension TimelineViewController {
|
||||||
|
|
||||||
var hideSeparators: Bool {
|
var hideSeparators: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .loadingIndicator, .publicTimelineDescription:
|
case .loadingIndicator, .publicTimelineDescription, .confirmLoadMore:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Combine
|
||||||
|
|
||||||
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
||||||
|
|
||||||
|
static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
||||||
|
|
||||||
// MARK: Subviews
|
// MARK: Subviews
|
||||||
|
|
||||||
private lazy var reblogLabel = EmojiLabel().configure {
|
private lazy var reblogLabel = EmojiLabel().configure {
|
||||||
|
|
Loading…
Reference in New Issue