2021-02-23 09:45:00 +01:00
|
|
|
//
|
|
|
|
// ProfileViewController.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by MainasuK Cirno on 2021-2-23.
|
|
|
|
//
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
import os.log
|
2021-02-23 09:45:00 +01:00
|
|
|
import UIKit
|
2021-04-01 08:39:15 +02:00
|
|
|
import Combine
|
|
|
|
import ActiveLabel
|
2021-02-23 09:45:00 +01:00
|
|
|
|
2021-04-28 14:10:17 +02:00
|
|
|
final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
2021-02-23 09:45:00 +01:00
|
|
|
|
|
|
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
|
|
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
|
|
var viewModel: ProfileViewModel!
|
|
|
|
|
2021-04-28 14:10:17 +02:00
|
|
|
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
private(set) lazy var cancelEditingBarButtonItem: UIBarButtonItem = {
|
|
|
|
let barButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ProfileViewController.cancelEditingBarButtonItemPressed(_:)))
|
|
|
|
barButtonItem.tintColor = .white
|
|
|
|
return barButtonItem
|
|
|
|
}()
|
|
|
|
|
2021-04-07 08:24:28 +02:00
|
|
|
private(set) lazy var settingBarButtonItem: UIBarButtonItem = {
|
|
|
|
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "gear"), style: .plain, target: self, action: #selector(ProfileViewController.settingBarButtonItemPressed(_:)))
|
|
|
|
barButtonItem.tintColor = .white
|
|
|
|
return barButtonItem
|
|
|
|
}()
|
|
|
|
|
|
|
|
private(set) lazy var shareBarButtonItem: UIBarButtonItem = {
|
|
|
|
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), style: .plain, target: self, action: #selector(ProfileViewController.shareBarButtonItemPressed(_:)))
|
|
|
|
barButtonItem.tintColor = .white
|
|
|
|
return barButtonItem
|
|
|
|
}()
|
|
|
|
|
|
|
|
private(set) lazy var favoriteBarButtonItem: UIBarButtonItem = {
|
|
|
|
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "star"), style: .plain, target: self, action: #selector(ProfileViewController.favoriteBarButtonItemPressed(_:)))
|
|
|
|
barButtonItem.tintColor = .white
|
|
|
|
return barButtonItem
|
|
|
|
}()
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
private(set) lazy var replyBarButtonItem: UIBarButtonItem = {
|
|
|
|
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrowshape.turn.up.left"), style: .plain, target: self, action: #selector(ProfileViewController.replyBarButtonItemPressed(_:)))
|
|
|
|
barButtonItem.tintColor = .white
|
|
|
|
return barButtonItem
|
|
|
|
}()
|
|
|
|
|
|
|
|
let moreMenuBarButtonItem: UIBarButtonItem = {
|
|
|
|
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil)
|
|
|
|
barButtonItem.tintColor = .white
|
|
|
|
return barButtonItem
|
|
|
|
}()
|
2021-04-01 08:39:15 +02:00
|
|
|
|
|
|
|
let refreshControl: UIRefreshControl = {
|
|
|
|
let refreshControl = UIRefreshControl()
|
2021-06-22 13:33:36 +02:00
|
|
|
refreshControl.tintColor = .white
|
2021-04-01 08:39:15 +02:00
|
|
|
return refreshControl
|
|
|
|
}()
|
|
|
|
|
|
|
|
let containerScrollView: UIScrollView = {
|
|
|
|
let scrollView = UIScrollView()
|
|
|
|
scrollView.scrollsToTop = false
|
|
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
|
|
scrollView.preservesSuperviewLayoutMargins = true
|
|
|
|
scrollView.delaysContentTouches = false
|
|
|
|
return scrollView
|
|
|
|
}()
|
|
|
|
|
|
|
|
let overlayScrollView: UIScrollView = {
|
|
|
|
let scrollView = UIScrollView()
|
|
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
|
|
scrollView.backgroundColor = .clear
|
|
|
|
scrollView.delaysContentTouches = false
|
|
|
|
return scrollView
|
|
|
|
}()
|
|
|
|
|
|
|
|
private(set) lazy var profileSegmentedViewController = ProfileSegmentedViewController()
|
2021-04-09 11:31:43 +02:00
|
|
|
private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = {
|
|
|
|
let viewController = ProfileHeaderViewController()
|
|
|
|
viewController.viewModel = ProfileHeaderViewModel(context: context)
|
|
|
|
return viewController
|
|
|
|
}()
|
2021-04-01 08:39:15 +02:00
|
|
|
private var profileBannerImageViewLayoutConstraint: NSLayoutConstraint!
|
|
|
|
|
|
|
|
private var contentOffsets: [Int: CGFloat] = [:]
|
|
|
|
var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation?
|
|
|
|
|
2021-04-09 13:44:48 +02:00
|
|
|
// title view nested in header
|
|
|
|
var titleView: DoubleTitleLabelNavigationBarTitleView {
|
|
|
|
profileHeaderViewController.titleView
|
|
|
|
}
|
2021-04-01 08:39:15 +02:00
|
|
|
|
|
|
|
deinit {
|
|
|
|
os_log("%{public}s[%{public}ld], %{public}s: deinit", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ProfileViewController {
|
|
|
|
|
|
|
|
func observeTableViewContentSize(scrollView: UIScrollView) -> NSKeyValueObservation {
|
|
|
|
updateOverlayScrollViewContentSize(scrollView: scrollView)
|
|
|
|
return scrollView.observe(\.contentSize, options: .new) { scrollView, change in
|
|
|
|
self.updateOverlayScrollViewContentSize(scrollView: scrollView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateOverlayScrollViewContentSize(scrollView: UIScrollView) {
|
|
|
|
let bottomPageHeight = max(scrollView.contentSize.height, self.containerScrollView.frame.height - ProfileHeaderViewController.headerMinHeight - self.containerScrollView.safeAreaInsets.bottom)
|
|
|
|
let headerViewHeight: CGFloat = profileHeaderViewController.view.frame.height
|
|
|
|
let contentSize = CGSize(
|
|
|
|
width: self.containerScrollView.contentSize.width,
|
|
|
|
height: bottomPageHeight + headerViewHeight
|
|
|
|
)
|
|
|
|
self.overlayScrollView.contentSize = contentSize
|
2021-04-02 12:13:45 +02:00
|
|
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: contentSize: %s", ((#file as NSString).lastPathComponent), #line, #function, contentSize.debugDescription)
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
2021-02-23 09:45:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
extension ProfileViewController {
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
2021-04-02 12:13:45 +02:00
|
|
|
return .lightContent
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override func viewSafeAreaInsetsDidChange() {
|
|
|
|
super.viewSafeAreaInsetsDidChange()
|
|
|
|
|
|
|
|
profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets)
|
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
override var isViewLoaded: Bool {
|
|
|
|
return super.isViewLoaded
|
|
|
|
}
|
|
|
|
|
2021-02-23 09:45:00 +01:00
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
2021-04-02 12:13:45 +02:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
let barAppearance = UINavigationBarAppearance()
|
|
|
|
barAppearance.configureWithTransparentBackground()
|
|
|
|
navigationItem.standardAppearance = barAppearance
|
|
|
|
navigationItem.compactAppearance = barAppearance
|
|
|
|
navigationItem.scrollEdgeAppearance = barAppearance
|
|
|
|
|
2021-04-09 13:44:48 +02:00
|
|
|
navigationItem.titleView = titleView
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
let editingAndUpdatingPublisher = Publishers.CombineLatest(
|
|
|
|
viewModel.isEditing.eraseToAnyPublisher(),
|
|
|
|
viewModel.isUpdating.eraseToAnyPublisher()
|
|
|
|
)
|
2021-04-09 11:46:20 +02:00
|
|
|
// note: not add .share() here
|
|
|
|
|
|
|
|
let barButtonItemHiddenPublisher = Publishers.CombineLatest3(
|
|
|
|
viewModel.isMeBarButtonItemsHidden.eraseToAnyPublisher(),
|
|
|
|
viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(),
|
|
|
|
viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher()
|
|
|
|
)
|
2021-04-09 11:31:43 +02:00
|
|
|
|
|
|
|
editingAndUpdatingPublisher
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isEditing, isUpdating in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.cancelEditingBarButtonItem.isEnabled = !isUpdating
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-04-29 11:13:13 +02:00
|
|
|
Publishers.CombineLatest4 (
|
2021-04-09 11:31:43 +02:00
|
|
|
viewModel.suspended.eraseToAnyPublisher(),
|
2021-04-29 11:13:13 +02:00
|
|
|
profileHeaderViewController.viewModel.isTitleViewDisplaying.eraseToAnyPublisher(),
|
2021-04-09 11:31:43 +02:00
|
|
|
editingAndUpdatingPublisher.eraseToAnyPublisher(),
|
|
|
|
barButtonItemHiddenPublisher.eraseToAnyPublisher()
|
|
|
|
)
|
2021-04-02 12:13:45 +02:00
|
|
|
.receive(on: DispatchQueue.main)
|
2021-04-29 11:13:13 +02:00
|
|
|
.sink { [weak self] suspended, isTitleViewDisplaying, tuple1, tuple2 in
|
2021-04-02 12:13:45 +02:00
|
|
|
guard let self = self else { return }
|
2021-04-09 11:31:43 +02:00
|
|
|
let (isEditing, _) = tuple1
|
|
|
|
let (isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden) = tuple2
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
var items: [UIBarButtonItem] = []
|
2021-04-07 08:24:28 +02:00
|
|
|
defer {
|
|
|
|
self.navigationItem.rightBarButtonItems = !items.isEmpty ? items : nil
|
|
|
|
}
|
|
|
|
|
2021-04-08 10:53:32 +02:00
|
|
|
guard !suspended else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
guard !isEditing else {
|
|
|
|
items.append(self.cancelEditingBarButtonItem)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-29 11:13:13 +02:00
|
|
|
guard !isTitleViewDisplaying else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-07 08:24:28 +02:00
|
|
|
guard isMeBarButtonItemsHidden else {
|
|
|
|
items.append(self.settingBarButtonItem)
|
|
|
|
items.append(self.shareBarButtonItem)
|
|
|
|
items.append(self.favoriteBarButtonItem)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
if !isReplyBarButtonItemHidden {
|
|
|
|
items.append(self.replyBarButtonItem)
|
|
|
|
}
|
|
|
|
if !isMoreMenuBarButtonItemHidden {
|
|
|
|
items.append(self.moreMenuBarButtonItem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
overlayScrollView.refreshControl = refreshControl
|
|
|
|
refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
|
|
|
|
|
|
|
let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter())
|
2021-04-06 10:43:08 +02:00
|
|
|
bind(userTimelineViewModel: postsUserTimelineViewModel)
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
|
2021-04-06 10:43:08 +02:00
|
|
|
bind(userTimelineViewModel: repliesUserTimelineViewModel)
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
|
2021-04-06 10:43:08 +02:00
|
|
|
bind(userTimelineViewModel: mediaUserTimelineViewModel)
|
2021-04-01 08:39:15 +02:00
|
|
|
|
|
|
|
profileSegmentedViewController.pagingViewController.viewModel = {
|
|
|
|
let profilePagingViewModel = ProfilePagingViewModel(
|
|
|
|
postsUserTimelineViewModel: postsUserTimelineViewModel,
|
|
|
|
repliesUserTimelineViewModel: repliesUserTimelineViewModel,
|
|
|
|
mediaUserTimelineViewModel: mediaUserTimelineViewModel
|
|
|
|
)
|
|
|
|
profilePagingViewModel.viewControllers.forEach { viewController in
|
|
|
|
if let viewController = viewController as? NeedsDependency {
|
|
|
|
viewController.context = context
|
|
|
|
viewController.coordinator = coordinator
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return profilePagingViewModel
|
|
|
|
}()
|
|
|
|
|
|
|
|
profileHeaderViewController.pageSegmentedControl.removeAllSegments()
|
|
|
|
profileSegmentedViewController.pagingViewController.viewModel.barItems.forEach { item in
|
|
|
|
let index = profileHeaderViewController.pageSegmentedControl.numberOfSegments
|
|
|
|
profileHeaderViewController.pageSegmentedControl.insertSegment(withTitle: item.title, at: index, animated: false)
|
|
|
|
}
|
|
|
|
profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = 0
|
|
|
|
|
|
|
|
overlayScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(overlayScrollView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
overlayScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
overlayScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
view.trailingAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.trailingAnchor),
|
|
|
|
view.bottomAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.bottomAnchor),
|
|
|
|
overlayScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
|
|
|
])
|
|
|
|
|
|
|
|
containerScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(containerScrollView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
containerScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
containerScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
view.trailingAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.trailingAnchor),
|
|
|
|
view.bottomAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.bottomAnchor),
|
|
|
|
containerScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
|
|
|
|
])
|
|
|
|
|
|
|
|
// add segmented list
|
|
|
|
addChild(profileSegmentedViewController)
|
|
|
|
profileSegmentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
containerScrollView.addSubview(profileSegmentedViewController.view)
|
|
|
|
profileSegmentedViewController.didMove(toParent: self)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
profileSegmentedViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
|
|
|
|
profileSegmentedViewController.view.trailingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.trailingAnchor),
|
|
|
|
profileSegmentedViewController.view.bottomAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.bottomAnchor),
|
|
|
|
profileSegmentedViewController.view.heightAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.heightAnchor),
|
|
|
|
])
|
|
|
|
|
|
|
|
// add header
|
|
|
|
addChild(profileHeaderViewController)
|
|
|
|
profileHeaderViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
containerScrollView.addSubview(profileHeaderViewController.view)
|
|
|
|
profileHeaderViewController.didMove(toParent: self)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
profileHeaderViewController.view.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
|
|
|
|
profileHeaderViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
|
|
|
|
containerScrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: profileHeaderViewController.view.trailingAnchor),
|
|
|
|
profileSegmentedViewController.view.topAnchor.constraint(equalTo: profileHeaderViewController.view.bottomAnchor),
|
|
|
|
])
|
|
|
|
|
|
|
|
containerScrollView.addGestureRecognizer(overlayScrollView.panGestureRecognizer)
|
|
|
|
overlayScrollView.layer.zPosition = .greatestFiniteMagnitude // make vision top-most
|
|
|
|
overlayScrollView.delegate = self
|
|
|
|
profileHeaderViewController.delegate = self
|
|
|
|
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
|
|
|
|
|
|
|
// bind view model
|
2021-04-09 13:44:48 +02:00
|
|
|
Publishers.CombineLatest(
|
|
|
|
viewModel.name.eraseToAnyPublisher(),
|
|
|
|
viewModel.statusesCount.eraseToAnyPublisher()
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] name, statusesCount in
|
|
|
|
guard let self = self else { return }
|
|
|
|
guard let title = name, let statusesCount = statusesCount,
|
|
|
|
let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else {
|
|
|
|
self.titleView.isHidden = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let subtitle = L10n.Scene.Profile.subtitle(formattedStatusCount)
|
|
|
|
self.titleView.update(title: title, subtitle: subtitle)
|
|
|
|
self.titleView.isHidden = false
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-02 12:13:45 +02:00
|
|
|
viewModel.name
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] name in
|
|
|
|
guard let self = self else { return }
|
2021-04-06 10:43:08 +02:00
|
|
|
self.navigationItem.title = name
|
2021-04-02 12:13:45 +02:00
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
Publishers.CombineLatest(
|
|
|
|
viewModel.bannerImageURL.eraseToAnyPublisher(),
|
|
|
|
viewModel.viewDidAppear.eraseToAnyPublisher()
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] bannerImageURL, _ in
|
|
|
|
guard let self = self else { return }
|
2021-04-02 12:13:45 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.bannerImageView.af.cancelImageRequest()
|
|
|
|
let placeholder = UIImage.placeholder(color: ProfileHeaderView.bannerImageViewPlaceholderColor)
|
2021-04-01 08:39:15 +02:00
|
|
|
guard let bannerImageURL = bannerImageURL else {
|
2021-04-02 12:13:45 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.bannerImageView.image = placeholder
|
2021-04-01 08:39:15 +02:00
|
|
|
return
|
|
|
|
}
|
2021-04-02 12:13:45 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.bannerImageView.af.setImage(
|
2021-04-01 08:39:15 +02:00
|
|
|
withURL: bannerImageURL,
|
|
|
|
placeholderImage: placeholder,
|
|
|
|
imageTransition: .crossDissolve(0.3),
|
|
|
|
runImageTransitionIfCached: false,
|
|
|
|
completion: { [weak self] response in
|
|
|
|
guard let self = self else { return }
|
2021-04-02 12:13:45 +02:00
|
|
|
guard let image = response.value else { return }
|
|
|
|
guard image.size.width > 1 && image.size.height > 1 else {
|
|
|
|
// restore to placeholder when image invalid
|
|
|
|
self.profileHeaderViewController.profileHeaderView.bannerImageView.image = placeholder
|
|
|
|
return
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-09 11:31:43 +02:00
|
|
|
viewModel.avatarImageURL
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.map { url in ProfileHeaderViewModel.ProfileInfo.ImageResource.url(url) }
|
|
|
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.avatarImageResource)
|
|
|
|
.store(in: &disposeBag)
|
2021-04-01 08:39:15 +02:00
|
|
|
viewModel.name
|
2021-04-09 11:31:43 +02:00
|
|
|
.map { $0 ?? "" }
|
2021-04-01 08:39:15 +02:00
|
|
|
.receive(on: DispatchQueue.main)
|
2021-04-09 11:31:43 +02:00
|
|
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name)
|
2021-04-01 08:39:15 +02:00
|
|
|
.store(in: &disposeBag)
|
2021-05-27 07:56:55 +02:00
|
|
|
viewModel.fileds
|
|
|
|
.removeDuplicates()
|
|
|
|
.map { fields -> [ProfileFieldItem.FieldValue] in
|
|
|
|
fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value) }
|
|
|
|
}
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.fields)
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
viewModel.emojiDict
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict)
|
|
|
|
.store(in: &disposeBag)
|
2021-04-01 08:39:15 +02:00
|
|
|
viewModel.username
|
|
|
|
.map { username in username.flatMap { "@" + $0 } ?? " " }
|
|
|
|
.receive(on: DispatchQueue.main)
|
2021-04-02 12:13:45 +02:00
|
|
|
.assign(to: \.text, on: profileHeaderViewController.profileHeaderView.usernameLabel)
|
2021-04-01 08:39:15 +02:00
|
|
|
.store(in: &disposeBag)
|
2021-04-30 08:55:02 +02:00
|
|
|
Publishers.CombineLatest(
|
|
|
|
viewModel.relationshipActionOptionSet,
|
|
|
|
viewModel.context.blockDomainService.blockedDomains
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] relationshipActionOptionSet,domains in
|
|
|
|
guard let self = self else { return }
|
|
|
|
guard let mastodonUser = self.viewModel.mastodonUser.value else {
|
|
|
|
self.moreMenuBarButtonItem.menu = nil
|
|
|
|
return
|
2021-04-02 12:13:45 +02:00
|
|
|
}
|
2021-05-06 12:19:24 +02:00
|
|
|
guard let currentMastodonUser = self.viewModel.currentMastodonUser.value else {
|
|
|
|
self.moreMenuBarButtonItem.menu = nil
|
|
|
|
return
|
|
|
|
}
|
2021-04-30 08:55:02 +02:00
|
|
|
guard let currentDomain = self.viewModel.domain.value else { return }
|
|
|
|
let isMuting = relationshipActionOptionSet.contains(.muting)
|
|
|
|
let isBlocking = relationshipActionOptionSet.contains(.blocking)
|
|
|
|
let isDomainBlocking = domains.contains(mastodonUser.domainFromAcct)
|
|
|
|
let needsShareAction = self.viewModel.isMeBarButtonItemsHidden.value
|
|
|
|
let isInSameDomain = mastodonUser.domainFromAcct == currentDomain
|
2021-05-06 12:19:24 +02:00
|
|
|
let isMyself = currentMastodonUser.id == mastodonUser.id
|
|
|
|
|
2021-04-30 08:55:02 +02:00
|
|
|
self.moreMenuBarButtonItem.menu = UserProviderFacade.createProfileActionMenu(
|
|
|
|
for: mastodonUser,
|
2021-05-06 12:19:24 +02:00
|
|
|
isMyself: isMyself,
|
2021-04-30 08:55:02 +02:00
|
|
|
isMuting: isMuting,
|
|
|
|
isBlocking: isBlocking,
|
|
|
|
isInSameDomain: isInSameDomain,
|
|
|
|
isDomainBlocking: isDomainBlocking,
|
|
|
|
provider: self,
|
|
|
|
cell: nil,
|
|
|
|
sourceView: nil,
|
|
|
|
barButtonItem: self.moreMenuBarButtonItem,
|
|
|
|
shareUser: needsShareAction ? mastodonUser : nil,
|
|
|
|
shareStatus: nil)
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
viewModel.isRelationshipActionButtonHidden
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isHidden in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.profileHeaderViewController.profileHeaderView.relationshipActionButton.isHidden = isHidden
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-09 11:31:43 +02:00
|
|
|
Publishers.CombineLatest3(
|
2021-04-02 12:13:45 +02:00
|
|
|
viewModel.relationshipActionOptionSet.eraseToAnyPublisher(),
|
2021-04-09 11:31:43 +02:00
|
|
|
viewModel.isEditing.eraseToAnyPublisher(),
|
|
|
|
viewModel.isUpdating.eraseToAnyPublisher()
|
2021-04-02 12:13:45 +02:00
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
2021-04-09 11:31:43 +02:00
|
|
|
.sink { [weak self] relationshipActionSet, isEditing, isUpdating in
|
2021-04-02 12:13:45 +02:00
|
|
|
guard let self = self else { return }
|
|
|
|
let friendshipButton = self.profileHeaderViewController.profileHeaderView.relationshipActionButton
|
|
|
|
if relationshipActionSet.contains(.edit) {
|
2021-04-09 11:31:43 +02:00
|
|
|
// check .edit state and set .editing when isEditing
|
|
|
|
friendshipButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
|
|
|
|
self.profileHeaderViewController.profileHeaderView.configure(state: isUpdating || isEditing ? .editing : .normal)
|
2021-04-02 12:13:45 +02:00
|
|
|
} else {
|
|
|
|
friendshipButton.configure(actionOptionSet: relationshipActionSet)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-09 11:31:43 +02:00
|
|
|
viewModel.isEditing
|
|
|
|
.handleEvents(receiveOutput: { [weak self] isEditing in
|
|
|
|
guard let self = self else { return }
|
2021-05-21 09:23:02 +02:00
|
|
|
// set firset responder for key command
|
|
|
|
if !isEditing {
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
|
|
self.profileSegmentedViewController.pagingViewController.becomeFirstResponder()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
// dismiss keyboard if needs
|
|
|
|
if !isEditing { self.view.endEditing(true) }
|
|
|
|
|
|
|
|
self.profileHeaderViewController.pageSegmentedControl.isEnabled = !isEditing
|
|
|
|
self.profileSegmentedViewController.view.isUserInteractionEnabled = !isEditing
|
|
|
|
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
|
|
|
|
animator.addAnimations {
|
|
|
|
self.profileSegmentedViewController.view.alpha = isEditing ? 0.2 : 1.0
|
2021-04-09 13:44:48 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.alpha = isEditing ? 0.2 : 1.0
|
2021-04-09 11:31:43 +02:00
|
|
|
}
|
|
|
|
animator.startAnimation()
|
|
|
|
})
|
|
|
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.isEditing)
|
|
|
|
.store(in: &disposeBag)
|
2021-04-08 10:53:32 +02:00
|
|
|
Publishers.CombineLatest3(
|
|
|
|
viewModel.isBlocking.eraseToAnyPublisher(),
|
|
|
|
viewModel.isBlockedBy.eraseToAnyPublisher(),
|
|
|
|
viewModel.suspended.eraseToAnyPublisher()
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isBlocking, isBlockedBy, suspended in
|
|
|
|
guard let self = self else { return }
|
|
|
|
let isNeedSetHidden = isBlocking || isBlockedBy || suspended
|
2021-04-09 11:31:43 +02:00
|
|
|
self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden
|
2021-04-08 10:53:32 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden
|
|
|
|
self.profileHeaderViewController.pageSegmentedControl.isHidden = isNeedSetHidden
|
|
|
|
self.viewModel.needsPagePinToTop.value = isNeedSetHidden
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-01 08:39:15 +02:00
|
|
|
viewModel.bioDescription
|
|
|
|
.receive(on: DispatchQueue.main)
|
2021-04-09 11:31:43 +02:00
|
|
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.note)
|
2021-04-01 08:39:15 +02:00
|
|
|
.store(in: &disposeBag)
|
|
|
|
viewModel.statusesCount
|
|
|
|
.sink { [weak self] count in
|
|
|
|
guard let self = self else { return }
|
2021-04-02 12:13:45 +02:00
|
|
|
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
|
2021-05-13 08:27:57 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true
|
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countPosts(count ?? 0)
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
viewModel.followingCount
|
|
|
|
.sink { [weak self] count in
|
|
|
|
guard let self = self else { return }
|
2021-04-02 12:13:45 +02:00
|
|
|
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
|
2021-05-13 08:27:57 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true
|
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowing(count ?? 0)
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
viewModel.followersCount
|
|
|
|
.sink { [weak self] count in
|
|
|
|
guard let self = self else { return }
|
2021-04-02 12:13:45 +02:00
|
|
|
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
|
2021-05-13 08:27:57 +02:00
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true
|
|
|
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0)
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-02 12:13:45 +02:00
|
|
|
|
|
|
|
profileHeaderViewController.profileHeaderView.delegate = self
|
2021-02-23 09:45:00 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
|
|
super.viewWillAppear(animated)
|
|
|
|
|
|
|
|
// set back button tint color in SceneCoordinator.present(scene:from:transition:)
|
|
|
|
|
|
|
|
// force layout to make banner image tweak take effect
|
|
|
|
view.layoutIfNeeded()
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
|
|
super.viewDidAppear(animated)
|
2021-04-02 12:13:45 +02:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
viewModel.viewDidAppear.send()
|
|
|
|
|
|
|
|
// set overlay scroll view initial content size
|
|
|
|
guard let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer else { return }
|
|
|
|
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: currentViewController.scrollView)
|
|
|
|
currentViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|
|
|
super.viewDidDisappear(animated)
|
2021-04-09 11:31:43 +02:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
currentPostTimelineTableViewContentSizeObservation = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-04-06 10:43:08 +02:00
|
|
|
extension ProfileViewController {
|
|
|
|
|
|
|
|
private func bind(userTimelineViewModel: UserTimelineViewModel) {
|
|
|
|
viewModel.domain.assign(to: \.value, on: userTimelineViewModel.domain).store(in: &disposeBag)
|
|
|
|
viewModel.userID.assign(to: \.value, on: userTimelineViewModel.userID).store(in: &disposeBag)
|
|
|
|
viewModel.isBlocking.assign(to: \.value, on: userTimelineViewModel.isBlocking).store(in: &disposeBag)
|
|
|
|
viewModel.isBlockedBy.assign(to: \.value, on: userTimelineViewModel.isBlockedBy).store(in: &disposeBag)
|
2021-04-08 10:53:32 +02:00
|
|
|
viewModel.suspended.assign(to: \.value, on: userTimelineViewModel.isSuspended).store(in: &disposeBag)
|
|
|
|
viewModel.name.assign(to: \.value, on: userTimelineViewModel.userDisplayName).store(in: &disposeBag)
|
2021-04-06 10:43:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
extension ProfileViewController {
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
@objc private func cancelEditingBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
viewModel.isEditing.value = false
|
|
|
|
}
|
|
|
|
|
2021-04-07 08:24:28 +02:00
|
|
|
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
2021-04-26 10:57:50 +02:00
|
|
|
guard let setting = context.settingService.currentSetting.value else { return }
|
|
|
|
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
|
|
|
|
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
2021-04-07 08:24:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
2021-04-08 12:52:35 +02:00
|
|
|
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
|
|
|
let activityViewController = UserProviderFacade.createActivityViewControllerForMastodonUser(mastodonUser: mastodonUser, dependency: self)
|
|
|
|
coordinator.present(
|
|
|
|
scene: .activityViewController(
|
|
|
|
activityViewController: activityViewController,
|
|
|
|
sourceView: nil,
|
|
|
|
barButtonItem: sender
|
|
|
|
),
|
|
|
|
from: self,
|
|
|
|
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
|
|
|
)
|
2021-04-07 08:24:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
let favoriteViewModel = FavoriteViewModel(context: context)
|
|
|
|
coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show)
|
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
2021-04-02 12:50:08 +02:00
|
|
|
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
|
|
|
let composeViewModel = ComposeViewModel(
|
|
|
|
context: context,
|
|
|
|
composeKind: .mention(mastodonUserObjectID: mastodonUser.objectID)
|
|
|
|
)
|
|
|
|
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
2021-04-02 12:13:45 +02:00
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
|
|
|
|
let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController
|
|
|
|
if let currentViewController = currentViewController as? UserTimelineViewController {
|
|
|
|
currentViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
|
|
|
|
}
|
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
|
|
sender.endRefreshing()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - UIScrollViewDelegate
|
|
|
|
extension ProfileViewController: UIScrollViewDelegate {
|
|
|
|
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
|
contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
|
|
|
|
let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
|
|
|
|
if scrollView.contentOffset.y < topMaxContentOffsetY {
|
|
|
|
self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
|
|
|
|
for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
|
|
|
|
postTimelineView.scrollView.contentOffset.y = 0
|
|
|
|
}
|
|
|
|
contentOffsets.removeAll()
|
|
|
|
} else {
|
|
|
|
containerScrollView.contentOffset.y = topMaxContentOffsetY
|
2021-04-08 10:53:32 +02:00
|
|
|
if viewModel.needsPagePinToTop.value {
|
|
|
|
// do nothing
|
|
|
|
} else {
|
|
|
|
if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
|
|
|
|
let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
|
|
|
|
customScrollViewContainerController.scrollView.contentOffset.y = contentOffsetY
|
|
|
|
}
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
2021-04-08 10:53:32 +02:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// elastically banner image
|
|
|
|
let headerScrollProgress = containerScrollView.contentOffset.y / topMaxContentOffsetY
|
2021-05-27 09:27:12 +02:00
|
|
|
let throttle = ProfileHeaderViewController.headerMinHeight / topMaxContentOffsetY
|
|
|
|
profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress, throttle: throttle)
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - ProfileHeaderViewControllerDelegate
|
|
|
|
extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
|
|
|
|
|
|
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) {
|
|
|
|
guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else {
|
|
|
|
// assertionFailure()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
updateOverlayScrollViewContentSize(scrollView: scrollView)
|
|
|
|
}
|
|
|
|
|
|
|
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int) {
|
|
|
|
profileSegmentedViewController.pagingViewController.scrollToPage(
|
|
|
|
.at(index: index),
|
|
|
|
animated: true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
|
|
|
switch entity.type {
|
|
|
|
case .url(_, _, let url, _):
|
|
|
|
guard let url = URL(string: url) else { return }
|
|
|
|
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
|
|
|
case .hashtag(let hashtag, _):
|
|
|
|
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
|
|
|
|
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - ProfilePagingViewControllerDelegate
|
|
|
|
extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
|
|
|
|
|
|
|
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController postTimelineViewController: ScrollViewContainer, atIndex index: Int) {
|
|
|
|
os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
|
|
|
|
|
2021-04-15 05:21:33 +02:00
|
|
|
// update segemented control
|
|
|
|
if index < profileHeaderViewController.pageSegmentedControl.numberOfSegments {
|
|
|
|
profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = index
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
// save content offset
|
|
|
|
overlayScrollView.contentOffset.y = contentOffsets[index] ?? containerScrollView.contentOffset.y
|
|
|
|
|
|
|
|
// setup observer and gesture fallback
|
|
|
|
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: postTimelineViewController.scrollView)
|
|
|
|
postTimelineViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - ProfileHeaderViewDelegate
|
|
|
|
extension ProfileViewController: ProfileHeaderViewDelegate {
|
|
|
|
|
2021-04-28 14:10:17 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) {
|
|
|
|
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
|
|
|
guard let avatar = imageView.image else { return }
|
|
|
|
|
|
|
|
let meta = MediaPreviewViewModel.ProfileAvatarImagePreviewMeta(
|
|
|
|
accountObjectID: mastodonUser.objectID,
|
|
|
|
preloadThumbnailImage: avatar
|
|
|
|
)
|
|
|
|
let pushTransitionItem = MediaPreviewTransitionItem(
|
|
|
|
source: .profileAvatar(profileHeaderView),
|
|
|
|
previewableViewController: self
|
|
|
|
)
|
|
|
|
pushTransitionItem.aspectRatio = CGSize(width: 100, height: 100)
|
|
|
|
pushTransitionItem.sourceImageView = imageView
|
|
|
|
pushTransitionItem.sourceImageViewCornerRadius = ProfileHeaderView.avatarImageViewCornerRadius
|
|
|
|
pushTransitionItem.initialFrame = {
|
|
|
|
let initialFrame = imageView.superview!.convert(imageView.frame, to: nil)
|
|
|
|
assert(initialFrame != .zero)
|
|
|
|
return initialFrame
|
|
|
|
}()
|
|
|
|
pushTransitionItem.image = avatar
|
|
|
|
|
|
|
|
let mediaPreviewViewModel = MediaPreviewViewModel(
|
|
|
|
context: context,
|
|
|
|
meta: meta,
|
|
|
|
pushTransitionItem: pushTransitionItem
|
|
|
|
)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.coordinator.present(scene: .mediaPreview(viewModel: mediaPreviewViewModel), from: self, transition: .custom(transitioningDelegate: self.mediaPreviewTransitionController))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 14:36:10 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) {
|
2021-04-29 11:05:35 +02:00
|
|
|
// not preview header banner when editing
|
|
|
|
guard !viewModel.isEditing.value else { return }
|
|
|
|
|
2021-04-28 14:36:10 +02:00
|
|
|
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
|
|
|
guard let header = imageView.image else { return }
|
|
|
|
|
|
|
|
let meta = MediaPreviewViewModel.ProfileBannerImagePreviewMeta(
|
|
|
|
accountObjectID: mastodonUser.objectID,
|
|
|
|
preloadThumbnailImage: header
|
|
|
|
)
|
|
|
|
let pushTransitionItem = MediaPreviewTransitionItem(
|
|
|
|
source: .profileBanner(profileHeaderView),
|
|
|
|
previewableViewController: self
|
|
|
|
)
|
|
|
|
pushTransitionItem.aspectRatio = header.size
|
|
|
|
pushTransitionItem.sourceImageView = imageView
|
|
|
|
pushTransitionItem.initialFrame = {
|
|
|
|
let initialFrame = imageView.superview!.convert(imageView.frame, to: nil)
|
|
|
|
assert(initialFrame != .zero)
|
|
|
|
return initialFrame
|
|
|
|
}()
|
|
|
|
pushTransitionItem.image = header
|
|
|
|
|
|
|
|
let mediaPreviewViewModel = MediaPreviewViewModel(
|
|
|
|
context: context,
|
|
|
|
meta: meta,
|
|
|
|
pushTransitionItem: pushTransitionItem
|
|
|
|
)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.coordinator.present(scene: .mediaPreview(viewModel: mediaPreviewViewModel), from: self, transition: .custom(transitioningDelegate: self.mediaPreviewTransitionController))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
|
|
|
|
let relationshipActionSet = viewModel.relationshipActionOptionSet.value
|
|
|
|
if relationshipActionSet.contains(.edit) {
|
2021-04-09 11:31:43 +02:00
|
|
|
guard !viewModel.isUpdating.value else { return }
|
|
|
|
|
|
|
|
if profileHeaderViewController.viewModel.isProfileInfoEdited() {
|
|
|
|
viewModel.isUpdating.value = true
|
|
|
|
profileHeaderViewController.viewModel.updateProfileInfo()
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] completion in
|
|
|
|
guard let self = self else { return }
|
|
|
|
switch completion {
|
|
|
|
case .failure(let error):
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update profile info fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
|
|
|
case .finished:
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update profile info success", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
}
|
|
|
|
self.viewModel.isUpdating.value = false
|
|
|
|
} receiveValue: { [weak self] _ in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.viewModel.isEditing.value = false
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
} else {
|
|
|
|
viewModel.isEditing.value.toggle()
|
|
|
|
}
|
2021-04-02 12:13:45 +02:00
|
|
|
} else {
|
|
|
|
guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
|
|
|
|
switch relationshipAction {
|
|
|
|
case .none:
|
|
|
|
break
|
2021-06-22 14:54:34 +02:00
|
|
|
case .follow, .request, .pending, .following:
|
2021-04-02 12:13:45 +02:00
|
|
|
UserProviderFacade.toggleUserFollowRelationship(provider: self)
|
|
|
|
.sink { _ in
|
2021-04-08 10:53:32 +02:00
|
|
|
// TODO: handle error
|
2021-04-02 12:13:45 +02:00
|
|
|
} receiveValue: { _ in
|
2021-04-08 10:53:32 +02:00
|
|
|
// do nothing
|
2021-04-02 12:13:45 +02:00
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
case .muting:
|
|
|
|
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
|
|
|
let name = mastodonUser.displayNameWithFallback
|
|
|
|
let alertController = UIAlertController(
|
|
|
|
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
|
|
|
|
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
|
|
|
|
preferredStyle: .alert
|
|
|
|
)
|
2021-06-22 14:54:34 +02:00
|
|
|
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
|
2021-04-02 12:13:45 +02:00
|
|
|
guard let self = self else { return }
|
2021-05-06 12:03:58 +02:00
|
|
|
UserProviderFacade.toggleUserMuteRelationship(provider: self, cell: nil)
|
2021-04-02 12:13:45 +02:00
|
|
|
.sink { _ in
|
|
|
|
// do nothing
|
|
|
|
} receiveValue: { _ in
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
.store(in: &self.context.disposeBag)
|
|
|
|
}
|
|
|
|
alertController.addAction(unmuteAction)
|
|
|
|
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
|
|
|
alertController.addAction(cancelAction)
|
|
|
|
present(alertController, animated: true, completion: nil)
|
|
|
|
case .blocking:
|
|
|
|
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
|
|
|
let name = mastodonUser.displayNameWithFallback
|
|
|
|
let alertController = UIAlertController(
|
|
|
|
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title,
|
|
|
|
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name),
|
|
|
|
preferredStyle: .alert
|
|
|
|
)
|
2021-06-22 14:54:34 +02:00
|
|
|
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
|
2021-04-02 12:13:45 +02:00
|
|
|
guard let self = self else { return }
|
2021-05-06 12:03:58 +02:00
|
|
|
UserProviderFacade.toggleUserBlockRelationship(provider: self, cell: nil)
|
2021-04-02 12:13:45 +02:00
|
|
|
.sink { _ in
|
|
|
|
// do nothing
|
|
|
|
} receiveValue: { _ in
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
.store(in: &self.context.disposeBag)
|
|
|
|
}
|
|
|
|
alertController.addAction(unblockAction)
|
|
|
|
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
|
|
|
alertController.addAction(cancelAction)
|
|
|
|
present(alertController, animated: true, completion: nil)
|
|
|
|
case .blocked:
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
assertionFailure()
|
|
|
|
}
|
|
|
|
}
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) {
|
|
|
|
switch entity.type {
|
|
|
|
case .url(_, _, let url, _):
|
|
|
|
guard let url = URL(string: url) else { return }
|
|
|
|
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
2021-05-27 07:56:55 +02:00
|
|
|
case .mention(_, let userInfo):
|
|
|
|
guard let href = userInfo?["href"] as? String,
|
|
|
|
let url = URL(string: href) else { return }
|
|
|
|
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
|
|
|
case .hashtag(let hashtag, _):
|
|
|
|
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
|
|
|
|
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
|
2021-04-02 12:13:45 +02:00
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
2021-05-27 07:56:55 +02:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) {
|
2021-04-01 08:39:15 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView) {
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - ScrollViewContainer
|
|
|
|
extension ProfileViewController: ScrollViewContainer {
|
|
|
|
var scrollView: UIScrollView { return overlayScrollView }
|
2021-02-23 09:45:00 +01:00
|
|
|
}
|
2021-05-21 09:23:02 +02:00
|
|
|
|
2021-05-21 11:42:14 +02:00
|
|
|
extension ProfileViewController {
|
|
|
|
|
|
|
|
override var keyCommands: [UIKeyCommand]? {
|
|
|
|
if !viewModel.isEditing.value {
|
|
|
|
return segmentedControlNavigateKeyCommands
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - SegmentedControlNavigateable
|
|
|
|
extension ProfileViewController: SegmentedControlNavigateable {
|
|
|
|
var navigateableSegmentedControl: UISegmentedControl {
|
|
|
|
profileHeaderViewController.pageSegmentedControl
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func segmentedControlNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
|
|
|
segmentedControlNavigateKeyCommandHandler(sender)
|
|
|
|
}
|
|
|
|
}
|