2020-07-05 20:17:56 +00:00
|
|
|
//
|
|
|
|
// 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!
|
|
|
|
|
2020-10-19 22:41:38 +00:00
|
|
|
// 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")
|
|
|
|
}
|
|
|
|
}
|
2020-07-05 20:17:56 +00:00
|
|
|
didSet {
|
|
|
|
pageControllers.forEach { $0.accountID = accountID }
|
2020-10-19 22:41:38 +00:00
|
|
|
loadAccount()
|
2020-07-05 20:17:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var accountUpdater: Cancellable?
|
|
|
|
|
|
|
|
private(set) var currentIndex: Int!
|
|
|
|
let pageControllers: [ProfileStatusesViewController]
|
|
|
|
var currentViewController: ProfileStatusesViewController {
|
|
|
|
pageControllers[currentIndex]
|
|
|
|
}
|
|
|
|
|
|
|
|
private var headerView: ProfileHeaderView!
|
|
|
|
|
2020-10-25 15:19:37 +00:00
|
|
|
private var hasAppeared = false
|
|
|
|
|
2020-07-05 20:17:56 +00:00
|
|
|
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")
|
|
|
|
}
|
2020-08-15 22:00:18 +00:00
|
|
|
|
|
|
|
deinit {
|
2020-10-19 22:41:38 +00:00
|
|
|
if let accountID = accountID {
|
|
|
|
mastodonController.persistentContainer.account(for: accountID)?.decrementReferenceCount()
|
|
|
|
}
|
2020-08-15 22:00:18 +00:00
|
|
|
}
|
2020-07-05 20:17:56 +00:00
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
|
|
view.backgroundColor = .systemBackground
|
|
|
|
|
2020-09-01 01:39:36 +00:00
|
|
|
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
2021-02-06 18:47:45 +00:00
|
|
|
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
2021-08-14 14:25:32 +00:00
|
|
|
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] (_) in
|
|
|
|
self?.composeDirectMentioning()
|
2021-02-06 18:47:45 +00:00
|
|
|
})
|
|
|
|
])
|
2021-06-27 14:31:02 +00:00
|
|
|
composeButton.isEnabled = mastodonController.loggedIn
|
2020-09-01 01:39:36 +00:00
|
|
|
navigationItem.rightBarButtonItem = composeButton
|
|
|
|
|
2020-07-05 20:17:56 +00:00
|
|
|
headerView = ProfileHeaderView.create()
|
|
|
|
headerView.delegate = self
|
|
|
|
|
|
|
|
selectPage(at: 0, animated: false)
|
|
|
|
|
|
|
|
currentViewController.tableView.tableHeaderView = headerView
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
headerView.widthAnchor.constraint(equalTo: view.widthAnchor),
|
|
|
|
])
|
|
|
|
|
2020-11-15 03:28:52 +00:00
|
|
|
addKeyCommand(MenuController.prevSubTabCommand)
|
|
|
|
addKeyCommand(MenuController.nextSubTabCommand)
|
2020-11-15 03:26:02 +00:00
|
|
|
|
2020-07-05 20:17:56 +00:00
|
|
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
|
|
|
.receive(on: DispatchQueue.main)
|
2021-06-26 20:51:54 +00:00
|
|
|
.filter { [weak self] in $0 == self?.accountID }
|
2020-07-05 20:17:56 +00:00
|
|
|
.sink { [weak self] (_) in self?.updateAccountUI() }
|
2020-08-15 22:00:18 +00:00
|
|
|
|
2020-10-19 22:41:38 +00:00
|
|
|
loadAccount()
|
2021-06-10 14:55:09 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2020-10-19 22:41:38 +00:00
|
|
|
}
|
|
|
|
|
2020-10-25 15:19:37 +00:00
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
|
|
super.viewDidAppear(animated)
|
|
|
|
|
|
|
|
hasAppeared = true
|
|
|
|
}
|
|
|
|
|
2020-10-19 22:41:38 +00:00
|
|
|
private func loadAccount() {
|
|
|
|
guard let accountID = accountID else { return }
|
2020-08-15 22:00:18 +00:00
|
|
|
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
2020-09-13 19:34:45 +00:00
|
|
|
updateAccountUI()
|
2020-08-15 22:00:18 +00:00
|
|
|
} 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 {
|
2020-09-13 19:34:45 +00:00
|
|
|
self.updateAccountUI()
|
2020-08-15 22:00:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-05 20:17:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func updateAccountUI() {
|
2020-10-19 22:41:38 +00:00
|
|
|
guard let accountID = accountID,
|
|
|
|
let account = mastodonController.persistentContainer.account(for: accountID) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-15 03:23:22 +00:00
|
|
|
if let currentAccountID = mastodonController.accountInfo?.id {
|
|
|
|
userActivity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
|
|
|
|
}
|
|
|
|
|
2020-10-19 22:41:38 +00:00
|
|
|
// Optionally invoke updateUI on headerView because viewDidLoad may not have been called yet
|
|
|
|
headerView?.updateUI(for: accountID)
|
2020-07-05 20:17:56 +00:00
|
|
|
navigationItem.title = account.displayNameWithoutCustomEmoji
|
2020-10-25 15:19:37 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
2020-10-19 22:41:38 +00:00
|
|
|
}
|
2020-07-05 20:17:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-11-15 03:26:02 +00:00
|
|
|
headerView.pagesSegmentedControl.selectedSegmentIndex = index
|
|
|
|
|
2020-07-05 20:17:56 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-01 01:39:36 +00:00
|
|
|
|
|
|
|
// MARK: Interaction
|
|
|
|
|
|
|
|
@objc private func composeMentioning() {
|
2020-10-19 22:41:38 +00:00
|
|
|
if let accountID = accountID,
|
|
|
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
2020-09-01 01:39:36 +00:00
|
|
|
compose(mentioningAcct: account.acct)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func composeDirectMentioning() {
|
2020-10-19 22:41:38 +00:00
|
|
|
if let accountID = accountID,
|
|
|
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
2020-09-01 01:39:36 +00:00
|
|
|
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
|
|
|
|
draft.visibility = .direct
|
|
|
|
compose(editing: draft)
|
|
|
|
}
|
|
|
|
}
|
2020-07-05 20:17:56 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ProfileViewController: TabBarScrollableViewController {
|
|
|
|
func tabBarScrollToTop() {
|
|
|
|
pageControllers[currentIndex].tabBarScrollToTop()
|
|
|
|
}
|
|
|
|
}
|
2020-11-15 03:26:02 +00:00
|
|
|
|
|
|
|
extension ProfileViewController: 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)
|
|
|
|
}
|
|
|
|
}
|