123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- //
- // HomeTimelineViewController.swift
- // Mastodon
- //
- // Created by sxiaojian on 2021/2/5.
- //
- import os.log
- import UIKit
- import AVKit
- import Combine
- import CoreData
- import CoreDataStack
- import GameplayKit
- import MastodonSDK
- import AlamofireImage
- import StoreKit
- import MastodonAsset
- import MastodonCore
- import MastodonUI
- import MastodonLocalization
- final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
-
- let logger = Logger(subsystem: "HomeTimelineViewController", category: "UI")
-
- weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
- weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
-
- var disposeBag = Set<AnyCancellable>()
- var viewModel: HomeTimelineViewModel!
-
- let mediaPreviewTransitionController = MediaPreviewTransitionController()
- let friendsAssetImageView: UIImageView = {
- let imageView = UIImageView()
- imageView.image = Asset.Asset.friends.image
- imageView.contentMode = .scaleAspectFill
- return imageView
- }()
-
- lazy var emptyView: UIStackView = {
- let emptyView = UIStackView()
- emptyView.axis = .vertical
- emptyView.distribution = .fill
- emptyView.isLayoutMarginsRelativeArrangement = true
- return emptyView
- }()
-
- let titleView = HomeTimelineNavigationBarTitleView()
-
- let settingBarButtonItem: UIBarButtonItem = {
- let barButtonItem = UIBarButtonItem()
- barButtonItem.tintColor = ThemeService.tintColor
- barButtonItem.image = Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate)
- barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.settings
- return barButtonItem
- }()
-
- let tableView: UITableView = {
- let tableView = ControlContainableTableView()
- tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
- tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
- tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
- tableView.rowHeight = UITableView.automaticDimension
- tableView.separatorStyle = .none
- tableView.backgroundColor = .clear
- return tableView
- }()
-
- let publishProgressView: UIProgressView = {
- let progressView = UIProgressView(progressViewStyle: .bar)
- progressView.alpha = 0
- return progressView
- }()
-
- let refreshControl = RefreshControl()
-
- deinit {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
- }
-
- }
- extension HomeTimelineViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
- title = L10n.Scene.HomeTimeline.title
- view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
- ThemeService.shared.currentTheme
- .receive(on: RunLoop.main)
- .sink { [weak self] theme in
- guard let self = self else { return }
- self.view.backgroundColor = theme.secondarySystemBackgroundColor
- }
- .store(in: &disposeBag)
- viewModel.$displaySettingBarButtonItem
- .receive(on: DispatchQueue.main)
- .sink { [weak self] displaySettingBarButtonItem in
- guard let self = self else { return }
- #if DEBUG
- // display debug menu
- self.navigationItem.rightBarButtonItem = {
- let barButtonItem = UIBarButtonItem()
- barButtonItem.image = UIImage(systemName: "ellipsis.circle")
- barButtonItem.menu = self.debugMenu
- return barButtonItem
- }()
- #else
- self.navigationItem.rightBarButtonItem = displaySettingBarButtonItem ? self.settingBarButtonItem : nil
- #endif
- }
- .store(in: &disposeBag)
- #if DEBUG
- // long press to trigger debug menu
- settingBarButtonItem.menu = debugMenu
- #else
- settingBarButtonItem.target = self
- settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
- #endif
-
- #if SNAPSHOT
- titleView.logoButton.menu = self.debugMenu
- titleView.button.menu = self.debugMenu
- #endif
-
- navigationItem.titleView = titleView
- titleView.delegate = self
-
- viewModel.homeTimelineNavigationBarTitleViewModel.state
- .removeDuplicates()
- .receive(on: DispatchQueue.main)
- .sink { [weak self] state in
- guard let self = self else { return }
- self.titleView.configure(state: state)
- }
- .store(in: &disposeBag)
-
- viewModel.homeTimelineNavigationBarTitleViewModel.state
- .removeDuplicates()
- .filter { $0 == .publishedButton }
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- guard UserDefaults.shared.lastVersionPromptedForReview == nil else { return }
- guard UserDefaults.shared.processCompletedCount > 3 else { return }
- guard let windowScene = self.view.window?.windowScene else { return }
- let version = UIApplication.appVersion()
- UserDefaults.shared.lastVersionPromptedForReview = version
- SKStoreReviewController.requestReview(in: windowScene)
- }
- .store(in: &disposeBag)
-
- tableView.refreshControl = refreshControl
- refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
- tableView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(tableView)
- NSLayoutConstraint.activate([
- tableView.topAnchor.constraint(equalTo: view.topAnchor),
- tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- ])
-
- // // layout publish progress
- publishProgressView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(publishProgressView)
- NSLayoutConstraint.activate([
- publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
- publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- ])
- viewModel.tableView = tableView
- tableView.delegate = self
- viewModel.setupDiffableDataSource(
- tableView: tableView,
- statusTableViewCellDelegate: self,
- timelineMiddleLoaderTableViewCellDelegate: self
- )
-
- // setup batch fetch
- viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
- viewModel.listBatchFetchViewModel.shouldFetch
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- guard self.view.window != nil else { return }
- self.viewModel.loadOldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
- }
- .store(in: &disposeBag)
-
- // bind refresh control
- viewModel.didLoadLatest
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- UIView.animate(withDuration: 0.5) { [weak self] in
- guard let self = self else { return }
- self.refreshControl.endRefreshing()
- } completion: { _ in }
- }
- .store(in: &disposeBag)
-
- context.publisherService.$currentPublishProgress
- .receive(on: DispatchQueue.main)
- .sink { [weak self] progress in
- guard let self = self else { return }
- let progress = Float(progress)
- guard progress > 0 else {
- let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut)
- dismissAnimator.addAnimations {
- self.publishProgressView.alpha = 0
- }
- dismissAnimator.addCompletion { _ in
- self.publishProgressView.setProgress(0, animated: false)
- }
- dismissAnimator.startAnimation()
- return
- }
- if self.publishProgressView.alpha == 0 {
- let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut)
- progressAnimator.addAnimations {
- self.publishProgressView.alpha = 1
- }
- progressAnimator.startAnimation()
- }
-
- self.publishProgressView.setProgress(progress, animated: true)
- }
- .store(in: &disposeBag)
-
- viewModel.timelineIsEmpty
- .receive(on: DispatchQueue.main)
- .sink { [weak self] isEmpty in
- if isEmpty {
- self?.showEmptyView()
- } else {
- self?.emptyView.removeFromSuperview()
- }
- }
- .store(in: &disposeBag)
-
- NotificationCenter.default
- .publisher(for: .statusBarTapped, object: nil)
- .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false)
- .sink { [weak self] notification in
- guard let self = self else { return }
- guard let _ = self.view.window else { return } // displaying
-
- // https://developer.limneos.net/index.php?ios=13.1.3&framework=UIKitCore.framework&header=UIStatusBarTapAction.h
- guard let action = notification.object as AnyObject?,
- let xPosition = action.value(forKey: "xPosition") as? Double
- else { return }
-
- let viewFrameInWindow = self.view.convert(self.view.frame, to: nil)
- guard xPosition >= viewFrameInWindow.minX && xPosition <= viewFrameInWindow.maxX else { return }
-
- // works on iOS 14
- self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): receive notification \(xPosition)")
- // check if scroll to top
- guard self.shouldRestoreScrollPosition() else { return }
- self.restorePositionWhenScrollToTop()
- }
- .store(in: &disposeBag)
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
- refreshControl.endRefreshing()
- tableView.deselectRow(with: transitionCoordinator, animated: animated)
-
- // needs trigger manually after onboarding dismiss
- setNeedsStatusBarAppearanceUpdate()
- }
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- viewModel.viewDidAppear.send()
- if let timestamp = viewModel.lastAutomaticFetchTimestamp {
- let now = Date()
- if now.timeIntervalSince(timestamp) > 60 {
- self.viewModel.lastAutomaticFetchTimestamp = now
- self.viewModel.homeTimelineNeedRefresh.send()
- } else {
- // do nothing
- }
- } else {
- self.viewModel.homeTimelineNeedRefresh.send()
- }
- }
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- coordinator.animate { _ in
- // do nothing
- } completion: { _ in
- // fix AutoLayout cell height not update after rotate issue
- self.viewModel.cellFrameCache.removeAllObjects()
- self.tableView.reloadData()
- }
- }
- }
- extension HomeTimelineViewController {
- func showEmptyView() {
- if emptyView.superview != nil {
- return
- }
- view.addSubview(emptyView)
- emptyView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- emptyView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
- emptyView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- emptyView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- emptyView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
- ])
-
- if emptyView.arrangedSubviews.count > 0 {
- return
- }
- let findPeopleButton: PrimaryActionButton = {
- let button = PrimaryActionButton()
- button.setTitle(L10n.Common.Controls.Actions.findPeople, for: .normal)
- button.addTarget(self, action: #selector(HomeTimelineViewController.findPeopleButtonPressed(_:)), for: .touchUpInside)
- return button
- }()
- NSLayoutConstraint.activate([
- findPeopleButton.heightAnchor.constraint(equalToConstant: 46)
- ])
-
- let manuallySearchButton: HighlightDimmableButton = {
- let button = HighlightDimmableButton()
- button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
- button.setTitle(L10n.Common.Controls.Actions.manuallySearch, for: .normal)
- button.setTitleColor(Asset.Colors.brand.color, for: .normal)
- button.addTarget(self, action: #selector(HomeTimelineViewController.manuallySearchButtonPressed(_:)), for: .touchUpInside)
- return button
- }()
- let topPaddingView = UIView()
- let bottomPaddingView = UIView()
- emptyView.addArrangedSubview(topPaddingView)
- emptyView.addArrangedSubview(friendsAssetImageView)
- emptyView.addArrangedSubview(bottomPaddingView)
- topPaddingView.translatesAutoresizingMaskIntoConstraints = false
- bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
- ])
- let buttonContainerStackView = UIStackView()
- emptyView.addArrangedSubview(buttonContainerStackView)
- buttonContainerStackView.isLayoutMarginsRelativeArrangement = true
- buttonContainerStackView.layoutMargins = UIEdgeInsets(top: 0, left: 32, bottom: 22, right: 32)
- buttonContainerStackView.axis = .vertical
- buttonContainerStackView.spacing = 17
- buttonContainerStackView.addArrangedSubview(findPeopleButton)
- buttonContainerStackView.addArrangedSubview(manuallySearchButton)
- }
- }
- extension HomeTimelineViewController {
-
- @objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) {
- let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext)
- suggestionAccountViewModel.delegate = viewModel
- _ = coordinator.present(
- scene: .suggestionAccount(viewModel: suggestionAccountViewModel),
- from: self,
- transition: .modal(animated: true, completion: nil)
- )
- }
-
- @objc private func manuallySearchButtonPressed(_ sender: UIButton) {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- let searchDetailViewModel = SearchDetailViewModel(authContext: viewModel.authContext)
- coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil))
- }
-
- @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- guard let setting = context.settingService.currentSetting.value else { return }
- let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
- coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
- }
- @objc private func refreshControlValueChanged(_ sender: RefreshControl) {
- guard viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) else {
- sender.endRefreshing()
- return
- }
- }
-
- @objc func signOutAction(_ sender: UIAction) {
- Task { @MainActor in
- try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
- self.coordinator.setup()
- }
- }
- }
- // MARK: - UIScrollViewDelegate
- extension HomeTimelineViewController {
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- switch scrollView {
- case tableView:
- viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
- default:
- break
- }
- }
-
- func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
- switch scrollView {
- case tableView:
-
- let indexPath = IndexPath(row: 0, section: 0)
- guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else {
- return true
- }
- // save position
- savePositionBeforeScrollToTop()
- // override by custom scrollToRow
- tableView.scrollToRow(at: indexPath, at: .top, animated: true)
- return false
- default:
- assertionFailure()
- return true
- }
- }
-
- private func savePositionBeforeScrollToTop() {
- // check save action interval
- // should not fast than 0.5s to prevent save when scrollToTop on-flying
- if let record = viewModel.scrollPositionRecord {
- let now = Date()
- guard now.timeIntervalSince(record.timestamp) > 0.5 else {
- // skip this save action
- return
- }
- }
-
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
- guard let anchorIndexPaths = tableView.indexPathsForVisibleRows?.sorted() else { return }
- guard !anchorIndexPaths.isEmpty else { return }
- let anchorIndexPath = anchorIndexPaths[anchorIndexPaths.count / 2]
- guard let anchorItem = diffableDataSource.itemIdentifier(for: anchorIndexPath) else { return }
-
- let offset: CGFloat = {
- guard let anchorCell = tableView.cellForRow(at: anchorIndexPath) else { return 0 }
- let cellFrameInView = tableView.convert(anchorCell.frame, to: view)
- return cellFrameInView.origin.y
- }()
- logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): save position record for \(anchorIndexPath) with offset: \(offset)")
- viewModel.scrollPositionRecord = HomeTimelineViewModel.ScrollPositionRecord(
- item: anchorItem,
- offset: offset,
- timestamp: Date()
- )
- }
-
- private func shouldRestoreScrollPosition() -> Bool {
- // check if scroll to top
- guard self.tableView.safeAreaInsets.top > 0 else { return false }
- let zeroOffset = -self.tableView.safeAreaInsets.top
- return abs(self.tableView.contentOffset.y - zeroOffset) < 2.0
- }
-
- private func restorePositionWhenScrollToTop() {
- guard let diffableDataSource = self.viewModel.diffableDataSource else { return }
- guard let record = self.viewModel.scrollPositionRecord,
- let indexPath = diffableDataSource.indexPath(for: record.item)
- else { return }
-
- tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
- viewModel.scrollPositionRecord = nil
- }
- }
- // MARK: - AuthContextProvider
- extension HomeTimelineViewController: AuthContextProvider {
- var authContext: AuthContext { viewModel.authContext }
- }
- // MARK: - UITableViewDelegate
- extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
- // sourcery:inline:HomeTimelineViewController.AutoGenerateTableViewDelegate
- // Generated using Sourcery
- // DO NOT EDIT
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- aspectTableView(tableView, didSelectRowAt: indexPath)
- }
- func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
- return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
- }
- func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
- return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
- }
- func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
- return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
- }
- func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
- aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
- }
- // sourcery:end
- }
- // MARK: - TimelineMiddleLoaderTableViewCellDelegate
- extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
- func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) {
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
- guard let indexPath = tableView.indexPath(for: cell) else { return }
- guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
- Task {
- await viewModel.loadMore(item: item)
- }
- }
- }
- // MARK: - ScrollViewContainer
- extension HomeTimelineViewController: ScrollViewContainer {
-
- var scrollView: UIScrollView { return tableView }
-
- func scrollToTop(animated: Bool) {
- if scrollView.contentOffset.y < scrollView.frame.height,
- viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
- (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
- !refreshControl.isRefreshing {
- scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -refreshControl.frame.height), size: CGSize(width: 1, height: 1)), animated: animated)
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
- self.refreshControl.beginRefreshing()
- self.refreshControl.sendActions(for: .valueChanged)
- }
- } else {
- let indexPath = IndexPath(row: 0, section: 0)
- guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return }
- // save position
- savePositionBeforeScrollToTop()
- tableView.scrollToRow(at: indexPath, at: .top, animated: true)
- }
- }
-
- }
- // MARK: - StatusTableViewCellDelegate
- extension HomeTimelineViewController: StatusTableViewCellDelegate { }
- // MARK: - HomeTimelineNavigationBarTitleViewDelegate
- extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate {
- func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) {
- if shouldRestoreScrollPosition() {
- restorePositionWhenScrollToTop()
- } else {
- savePositionBeforeScrollToTop()
- scrollToTop(animated: true)
- }
- }
-
- func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) {
- switch titleView.state {
- case .newPostButton:
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
- let indexPath = IndexPath(row: 0, section: 0)
- guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return }
-
- savePositionBeforeScrollToTop()
- tableView.scrollToRow(at: indexPath, at: .top, animated: true)
- case .offlineButton:
- // TODO: retry
- break
- case .publishedButton:
- break
- default:
- break
- }
- }
- }
- extension HomeTimelineViewController {
- override var keyCommands: [UIKeyCommand]? {
- return navigationKeyCommands + statusNavigationKeyCommands
- }
- }
- // MARK: - StatusTableViewControllerNavigateable
- extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
- @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
- navigateKeyCommandHandler(sender)
- }
- @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
- statusKeyCommandHandler(sender)
- }
- }
|