forked from shadowfacts/Tusker
276 lines
12 KiB
Swift
276 lines
12 KiB
Swift
//
|
|
// ProfileViewController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 7/3/20.
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
import Combine
|
|
|
|
class ProfileViewController: UIPageViewController {
|
|
|
|
weak var mastodonController: MastodonController!
|
|
|
|
// This property is optional because MyProfileViewController may not have the user's account ID
|
|
// when first constructed. It should never be set to nil.
|
|
var accountID: String? {
|
|
willSet {
|
|
if newValue == nil {
|
|
fatalError("Do not set ProfileViewController.accountID to nil")
|
|
}
|
|
}
|
|
didSet {
|
|
pageControllers.forEach { $0.accountID = accountID }
|
|
loadAccount()
|
|
}
|
|
}
|
|
|
|
private var accountUpdater: Cancellable?
|
|
|
|
private(set) var currentIndex: Int!
|
|
let pageControllers: [ProfileStatusesViewController]
|
|
var currentViewController: ProfileStatusesViewController {
|
|
pageControllers[currentIndex]
|
|
}
|
|
|
|
private var headerView: ProfileHeaderView!
|
|
|
|
private var hasAppeared = false
|
|
|
|
init(accountID: String?, mastodonController: MastodonController) {
|
|
self.accountID = accountID
|
|
self.mastodonController = mastodonController
|
|
|
|
self.pageControllers = [
|
|
ProfileStatusesViewController(accountID: accountID, kind: .statuses, mastodonController: mastodonController),
|
|
ProfileStatusesViewController(accountID: accountID, kind: .withReplies, mastodonController: mastodonController),
|
|
ProfileStatusesViewController(accountID: accountID, kind: .onlyMedia, mastodonController: mastodonController)
|
|
]
|
|
|
|
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
if let accountID = accountID {
|
|
mastodonController.persistentContainer.account(for: accountID)?.decrementReferenceCount()
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
view.backgroundColor = .systemBackground
|
|
|
|
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
|
if #available(iOS 14.0, *) {
|
|
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
|
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
|
self.composeDirectMentioning()
|
|
})
|
|
])
|
|
}
|
|
navigationItem.rightBarButtonItem = composeButton
|
|
|
|
headerView = ProfileHeaderView.create()
|
|
headerView.delegate = self
|
|
|
|
selectPage(at: 0, animated: false)
|
|
|
|
currentViewController.tableView.tableHeaderView = headerView
|
|
|
|
NSLayoutConstraint.activate([
|
|
headerView.widthAnchor.constraint(equalTo: view.widthAnchor),
|
|
])
|
|
|
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
|
.filter { [weak self] in $0 == self?.accountID }
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] (_) in self?.updateAccountUI() }
|
|
|
|
loadAccount()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
hasAppeared = true
|
|
}
|
|
|
|
private func loadAccount() {
|
|
guard let accountID = accountID else { return }
|
|
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
|
updateAccountUI()
|
|
} else {
|
|
let req = Client.getAccount(id: accountID)
|
|
mastodonController.run(req) { [weak self] (response) in
|
|
guard let self = self else { return }
|
|
guard case let .success(account, _) = response else { fatalError() }
|
|
self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (account) in
|
|
DispatchQueue.main.async {
|
|
self.updateAccountUI()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateAccountUI() {
|
|
guard let accountID = accountID,
|
|
let account = mastodonController.persistentContainer.account(for: accountID) else {
|
|
return
|
|
}
|
|
|
|
// Optionally invoke updateUI on headerView because viewDidLoad may not have been called yet
|
|
headerView?.updateUI(for: accountID)
|
|
navigationItem.title = account.displayNameWithoutCustomEmoji
|
|
|
|
// Only call updateUI on the individual page controllers if the account is loaded after the profile VC has appeared on screen.
|
|
// Otherwise, fi the page view controllers do something with the table view before they appear, the table view doesn't load
|
|
// its cells until the user begins to scroll.
|
|
if hasAppeared {
|
|
pageControllers.forEach {
|
|
$0.updateUI(account: account)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func selectPage(at index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
|
let direction: UIPageViewController.NavigationDirection = currentIndex == nil || index - currentIndex > 0 ? .forward : .reverse
|
|
currentIndex = index
|
|
|
|
guard let old = viewControllers?.first as? ProfileStatusesViewController else {
|
|
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
|
|
// since it will be added in viewDidLoad
|
|
setViewControllers([pageControllers[index]], direction: direction, animated: animated, completion: completion)
|
|
return
|
|
}
|
|
let new = pageControllers[index]
|
|
|
|
let headerHeight = self.headerView.bounds.height
|
|
|
|
// Store old's content offset so it can be transferred to new
|
|
let prevOldContentOffset = old.tableView.contentOffset
|
|
// Remove the header, inset the table content by the same amount, and adjust the offset so the cells don't move
|
|
old.tableView.tableHeaderView = nil
|
|
old.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
|
|
old.tableView.contentOffset.y -= headerHeight
|
|
|
|
// Add the header to ourself temporarily, and constrain it to the same position it was in
|
|
self.view.addSubview(self.headerView)
|
|
let tempTopConstraint = self.headerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -(prevOldContentOffset.y + old.tableView.safeAreaInsets.top))
|
|
NSLayoutConstraint.activate([
|
|
self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
|
|
tempTopConstraint
|
|
])
|
|
|
|
// Setup the inset in new, in case it hasn't been already
|
|
new.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
|
|
// Match the scroll positions
|
|
new.tableView.contentOffset = old.tableView.contentOffset
|
|
|
|
// Actually switch pages
|
|
setViewControllers([pageControllers[index]], direction: direction, animated: animated) { (finished) in
|
|
// Defer everything one run-loop iteration, otherwise altering the tableView's contentInset/Offset causes it to jump around during the animation
|
|
DispatchQueue.main.async {
|
|
// Move the header to the new table view
|
|
new.tableView.tableHeaderView = self.headerView
|
|
// Remove the inset, and set the offset back to old's original one, prior to removing the header
|
|
new.tableView.contentInset = .zero
|
|
new.tableView.contentOffset = prevOldContentOffset
|
|
|
|
// Deactivate the top constraint, otherwise it sticks around
|
|
tempTopConstraint.isActive = false
|
|
// Re-add the width constraint since it was removed by re-parenting the view
|
|
// Why was the width constraint removed, but the top one not? Good question, I have no idea.
|
|
NSLayoutConstraint.activate([
|
|
self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor)
|
|
])
|
|
|
|
// Layout and update the table view, otherwise the content jumps around when first scrolling it,
|
|
// if old was not scrolled all the way to the top
|
|
new.tableView.layoutIfNeeded()
|
|
UIView.performWithoutAnimation {
|
|
new.tableView.performBatchUpdates(nil, completion: nil)
|
|
}
|
|
|
|
completion?(finished)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Interaction
|
|
|
|
@objc private func composeMentioning() {
|
|
if let accountID = accountID,
|
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
|
compose(mentioningAcct: account.acct)
|
|
}
|
|
}
|
|
|
|
private func composeDirectMentioning() {
|
|
if let accountID = accountID,
|
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
|
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
|
|
draft.visibility = .direct
|
|
compose(editing: draft)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension ProfileViewController: TuskerNavigationDelegate {
|
|
var apiController: MastodonController { mastodonController }
|
|
}
|
|
|
|
extension ProfileViewController: ProfileHeaderViewDelegate {
|
|
func profileHeader(_ view: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) {
|
|
// disable user interaction on segmented control while switching pages to prevent
|
|
// race condition from trying to switch to multiple pages simultaneously
|
|
view.pagesSegmentedControl.isUserInteractionEnabled = false
|
|
selectPage(at: newIndex, animated: true) { (finished) in
|
|
view.pagesSegmentedControl.isUserInteractionEnabled = true
|
|
}
|
|
}
|
|
|
|
func profileHeader(_ view: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) {
|
|
let account = mastodonController.persistentContainer.account(for: accountID)!
|
|
|
|
func showActivityController(activities: [UIActivity]) {
|
|
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: account.url)
|
|
activityController.popoverPresentationController?.sourceView = sourceView
|
|
self.present(activityController, animated: true)
|
|
}
|
|
|
|
if account.id == mastodonController.account.id {
|
|
showActivityController(activities: [OpenInSafariActivity()])
|
|
} else {
|
|
let request = Client.getRelationships(accounts: [account.id])
|
|
mastodonController.run(request) { (response) in
|
|
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
|
if case let .success(results, _) = response, let relationship = results.first {
|
|
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
|
customActivities.insert(toggleFollowActivity, at: 0)
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
showActivityController(activities: customActivities)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ProfileViewController: TabBarScrollableViewController {
|
|
func tabBarScrollToTop() {
|
|
pageControllers[currentIndex].tabBarScrollToTop()
|
|
}
|
|
}
|