From 6af94352e2f4af329d0d405ed4244f1a516d7506 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Apr 2024 14:27:06 +0200 Subject: [PATCH 01/12] Remove NavigationBarTitleView and add first draft of TimelineStatusPill (IOS-234) --- Mastodon.xcodeproj/project.pbxproj | 20 +- .../HomeTimelineViewController.swift | 80 +------ ...omeTimelineViewModel+LoadLatestState.swift | 8 +- ...omeTimelineViewModel+LoadOldestState.swift | 5 +- .../HomeTimeline/HomeTimelineViewModel.swift | 12 - .../HomeTimeline/TimelineStatusPill.swift | 73 ++++++ .../HomeTimelineNavigationBarTitleView.swift | 221 ------------------ ...eTimelineNavigationBarTitleViewModel.swift | 182 --------------- 8 files changed, 90 insertions(+), 511 deletions(-) create mode 100644 Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift delete mode 100644 Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift delete mode 100644 Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d4139d7a9..9c96d3929 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -91,8 +91,6 @@ 2D7867192625B77500211898 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7867182625B77500211898 /* NotificationItem.swift */; }; 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; }; 2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; }; - 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; }; - 2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; }; 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; }; @@ -154,6 +152,7 @@ D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; }; D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; }; D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; }; + D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */; }; D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; }; D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; }; D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */; }; @@ -715,8 +714,6 @@ 2D7867182625B77500211898 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = ""; }; 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = ""; }; - 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = ""; }; - 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = ""; }; 2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = ""; }; 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewController.swift; sourceTree = ""; }; @@ -779,6 +776,7 @@ D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewController.swift; sourceTree = ""; }; D8318A892A4468DC00C0FB73 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = ""; tabWidth = 4; }; + D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = ""; }; D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = ""; }; D84C09A02B0F9E41009E685E /* How-it-works.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "How-it-works.md"; sourceTree = ""; }; @@ -1501,13 +1499,13 @@ 2D38F1D325CD463600561493 /* HomeTimeline */ = { isa = PBXGroup; children = ( - DB1F239626117C360057430E /* View */, 2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */, DB697DD8278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift */, 2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */, 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */, 2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */, 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */, + D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */, ); path = HomeTimeline; sourceTree = ""; @@ -1983,15 +1981,6 @@ path = TableView; sourceTree = ""; }; - DB1F239626117C360057430E /* View */ = { - isa = PBXGroup; - children = ( - 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */, - 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */, - ); - path = View; - sourceTree = ""; - }; DB3D0FF725BAA68500EAA174 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -3484,7 +3473,6 @@ DB0617FF27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift in Sources */, DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */, DB0FCB982797F6BF006C02E2 /* UserTableViewCell+ViewModel.swift in Sources */, - 2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */, DB697DD6278F4C29004EF2F7 /* DataSourceProvider.swift in Sources */, DB0FCB8E2796C0B7006C02E2 /* TrendCollectionViewCell.swift in Sources */, 0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */, @@ -3494,7 +3482,6 @@ DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, D8FAAE432AD047B200DC1832 /* AboutInstanceTableFooterView.swift in Sources */, D808B94E296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift in Sources */, - 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, @@ -3763,6 +3750,7 @@ DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, + D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */, D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 100b41300..da8a1c747 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -44,8 +44,6 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media return emptyView }() - let titleView = HomeTimelineNavigationBarTitleView() - lazy var timelineSelectorButton = { let button = UIButton(type: .custom) button.setAttributedTitle( @@ -101,6 +99,8 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media }() let refreshControl = RefreshControl() + let timelinePill = TimelineStatusPill() + private func generateTimeSelectorMenu() -> UIMenu { let showFollowingAction = UIAction(title: L10n.Scene.HomeTimeline.TimelineMenu.following, image: .init(systemName: "house")) { [weak self] _ in @@ -170,33 +170,6 @@ extension HomeTimelineViewController { settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: timelineSelectorButton) - -// 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) @@ -326,6 +299,15 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) +// timelinePill.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(timelinePill) +// +// // has to up updated and animated +// timelinePill.update(with: .postSent) +// NSLayoutConstraint.activate([ +// timelinePill.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), +// timelinePill.centerXAnchor.constraint(equalTo: view.centerXAnchor), +// ]) } override func viewWillAppear(_ animated: Bool) { @@ -480,15 +462,6 @@ extension HomeTimelineViewController { } // 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: @@ -644,37 +617,6 @@ extension HomeTimelineViewController: ScrollViewContainer { // 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 diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index aca9f5f45..97093660d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -129,7 +129,6 @@ extension HomeTimelineViewModel.LoadLatestState { } await enter(state: Idle.self) - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.finished) // stop refresher if no new statuses let statuses = response.value @@ -137,11 +136,7 @@ extension HomeTimelineViewModel.LoadLatestState { if newStatuses.isEmpty { viewModel.didLoadLatest.send() - } else { - if !latestStatusIDs.isEmpty { - viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming() - } - + } else { viewModel.dataController.records = { var newRecords: [MastodonFeed] = newStatuses.map { MastodonFeed.fromStatus(.fromEntity($0), kind: .home) @@ -168,7 +163,6 @@ extension HomeTimelineViewModel.LoadLatestState { } catch { await enter(state: Idle.self) viewModel.didLoadLatest.send() - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.failure(error)) } } // end Task } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 668095b24..4436c9b79 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -83,12 +83,9 @@ extension HomeTimelineViewModel.LoadOldestState { } else { await self.enter(state: Idle.self) } - - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.finished) - + } catch { await self.enter(state: Fail.self) - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.failure(error)) } } // end Task } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index a67d44f8d..e934fe1d0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -25,7 +25,6 @@ final class HomeTimelineViewModel: NSObject { let context: AppContext let authContext: AuthContext let dataController: FeedDataController - let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let listBatchFetchViewModel = ListBatchFetchViewModel() var presentedSuggestions = false @@ -81,7 +80,6 @@ final class HomeTimelineViewModel: NSObject { self.context = context self.authContext = authContext self.dataController = FeedDataController(context: context, authContext: authContext) - self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() self.dataController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox).map { MastodonFeed.fromStatus($0, kind: .home) @@ -92,16 +90,6 @@ final class HomeTimelineViewModel: NSObject { self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self) } .store(in: &disposeBag) - - // refresh after publish post - homeTimelineNavigationBarTitleViewModel.isPublished - .delay(for: 2, scheduler: DispatchQueue.main) - .sink { [weak self] isPublished in - guard let self = self else { return } - self.homeTimelineNeedRefresh.send() - } - .store(in: &disposeBag) - self.dataController.$records .removeDuplicates() .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift new file mode 100644 index 000000000..e24c512f9 --- /dev/null +++ b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift @@ -0,0 +1,73 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonAsset + +class TimelineStatusPill: UIButton { + + func update(with state: State) { + var configuration = UIButton.Configuration.filled() + + + configuration.attributedTitle = AttributedString( + state.title, attributes: AttributeContainer( + [ + .font: UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)), + .foregroundColor: UIColor.white + ] + )) + + let image = state.image? + .withConfiguration(UIImage.SymbolConfiguration(paletteColors: [.white])) + .withConfiguration(UIImage.SymbolConfiguration(textStyle: .subheadline)) + .withConfiguration(UIImage.SymbolConfiguration(pointSize: 12, weight: .bold, scale: .medium)) + + configuration.image = image + configuration.imagePadding = 8 + configuration.baseBackgroundColor = state.backgroundColor + configuration.cornerStyle = .capsule + + self.configuration = configuration + } + + public enum State { + case newPosts + case postSent + case offline + + var image: UIImage? { + switch self { + case .newPosts: + return UIImage(systemName: "chevron.up") + case .postSent: + return UIImage(systemName: "checkmark") + case .offline: + return UIImage(systemName: "bolt.horizontal.fill") + } + } + + var backgroundColor: UIColor { + switch self { + case .newPosts: + return Asset.Colors.Brand.blurple.color + case .postSent: + return .systemGreen + case .offline: + return .systemGray + } + } + + var title: String { + //TODO: Localization + switch self { + case .newPosts: + return "New Posts" + case .postSent: + return "Post Sent" + case .offline: + return "Offline" + } + } + } + +} diff --git a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift deleted file mode 100644 index 28475c2d7..000000000 --- a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleView.swift +++ /dev/null @@ -1,221 +0,0 @@ -// -// HomeTimelineNavigationBarTitleView.swift -// Mastodon -// -// Created by sxiaojian on 2021/3/15. -// - -import UIKit -import MastodonUI -import MastodonAsset -import MastodonLocalization - -protocol HomeTimelineNavigationBarTitleViewDelegate: AnyObject { - func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) - func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) -} - -final class HomeTimelineNavigationBarTitleView: UIView { - - let containerView = UIStackView() - - let logoButton = HighlightDimmableButton() - let button = RoundedEdgesButton() - let label = UILabel() - - // input - private var blockingState: HomeTimelineNavigationBarTitleViewModel.State? - weak var delegate: HomeTimelineNavigationBarTitleViewDelegate? - - // output - private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logo - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension HomeTimelineNavigationBarTitleView { - private func _init() { - containerView.translatesAutoresizingMaskIntoConstraints = false - addSubview(containerView) - containerView.pinToParent() - - containerView.addArrangedSubview(logoButton) - button.translatesAutoresizingMaskIntoConstraints = false - containerView.addArrangedSubview(button) - NSLayoutConstraint.activate([ - button.heightAnchor.constraint(equalToConstant: 24).priority(.defaultHigh) - ]) - containerView.addArrangedSubview(label) - - configure(state: .logo) - logoButton.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.logoButtonDidPressed(_:)), for: .touchUpInside) - button.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.buttonDidPressed(_:)), for: .touchUpInside) - - logoButton.accessibilityIdentifier = "TitleButton" - logoButton.accessibilityTraits = [.header, .button] - button.accessibilityIdentifier = "TitleButton" - } -} - -extension HomeTimelineNavigationBarTitleView { - @objc private func logoButtonDidPressed(_ sender: UIButton) { - delegate?.homeTimelineNavigationBarTitleView(self, logoButtonDidPressed: sender) - } - - @objc private func buttonDidPressed(_ sender: UIButton) { - delegate?.homeTimelineNavigationBarTitleView(self, buttonDidPressed: sender) - } -} - -extension HomeTimelineNavigationBarTitleView { - - func resetContainer() { - logoButton.isHidden = true - button.isHidden = true - label.isHidden = true - } - - func configure(state: HomeTimelineNavigationBarTitleViewModel.State) { - self.state = state - - // check state block or not - guard blockingState == nil else { - return - } - - resetContainer() - - switch state { - case .logo: - logoButton.tintColor = Asset.Colors.Label.primary.color - logoButton.setImage(Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate), for: .normal) - logoButton.contentMode = .center - logoButton.isHidden = false - logoButton.accessibilityLabel = L10n.Scene.HomeTimeline.NavigationBarState.Accessibility.logoLabel // TODO :i18n - logoButton.accessibilityHint = L10n.Scene.HomeTimeline.NavigationBarState.Accessibility.logoHint - case .newPostButton: - configureButton( - title: L10n.Scene.HomeTimeline.NavigationBarState.newPosts, - textColor: .white, - backgroundColor: Asset.Colors.Brand.blurple.color - ) - button.isHidden = false - button.accessibilityLabel = L10n.Scene.HomeTimeline.NavigationBarState.newPosts - case .offlineButton: - configureButton( - title: L10n.Scene.HomeTimeline.NavigationBarState.offline, - textColor: .white, - backgroundColor: Asset.Colors.danger.color - ) - button.isHidden = false - button.accessibilityLabel = L10n.Scene.HomeTimeline.NavigationBarState.offline - case .publishingPostLabel: - label.font = .systemFont(ofSize: 17, weight: .semibold) - label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Scene.HomeTimeline.NavigationBarState.publishing - label.textAlignment = .center - label.isHidden = false - button.accessibilityLabel = L10n.Scene.HomeTimeline.NavigationBarState.publishing - case .publishedButton: - blockingState = state - configureButton( - title: L10n.Scene.HomeTimeline.NavigationBarState.published, - textColor: .white, - backgroundColor: Asset.Colors.successGreen.color - ) - button.isHidden = false - button.accessibilityLabel = L10n.Scene.HomeTimeline.NavigationBarState.published - - let presentDuration: TimeInterval = 0.33 - let scaleAnimator = UIViewPropertyAnimator(duration: presentDuration, timingParameters: UISpringTimingParameters()) - button.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) - scaleAnimator.addAnimations { - self.button.transform = .identity - } - let alphaAnimator = UIViewPropertyAnimator(duration: presentDuration, curve: .easeInOut) - button.alpha = 0.3 - alphaAnimator.addAnimations { - self.button.alpha = 1 - } - scaleAnimator.startAnimation() - alphaAnimator.startAnimation() - - let dismissDuration: TimeInterval = 3 - let dissolveAnimator = UIViewPropertyAnimator(duration: dismissDuration, curve: .easeInOut) - dissolveAnimator.addAnimations({ - self.button.alpha = 0 - }, delayFactor: 0.9) // at 2.7s - dissolveAnimator.addCompletion { _ in - self.blockingState = nil - self.configure(state: self.state) - self.button.alpha = 1 - } - dissolveAnimator.startAnimation() - } - } - - private func configureButton(title: String, textColor: UIColor, backgroundColor: UIColor) { - button.setBackgroundImage(.placeholder(color: backgroundColor), for: .normal) - button.setBackgroundImage(.placeholder(color: backgroundColor.withAlphaComponent(0.5)), for: .highlighted) - button.setTitleColor(textColor, for: .normal) - button.setTitleColor(textColor.withAlphaComponent(0.5), for: .highlighted) - button.setTitle(title, for: .normal) - button.contentEdgeInsets = UIEdgeInsets(top: 1, left: 16, bottom: 1, right: 16) - button.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold) - } - -} - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct HomeTimelineNavigationBarTitleView_Previews: PreviewProvider { - - static var previews: some View { - Group { - UIViewPreview(width: 375) { - let titleView = HomeTimelineNavigationBarTitleView() - titleView.configure(state: .logo) - return titleView - } - .previewLayout(.fixed(width: 375, height: 44)) - UIViewPreview(width: 150) { - let titleView = HomeTimelineNavigationBarTitleView() - titleView.configure(state: .newPostButton) - return titleView - } - .previewLayout(.fixed(width: 150, height: 24)) - UIViewPreview(width: 120) { - let titleView = HomeTimelineNavigationBarTitleView() - titleView.configure(state: .offlineButton) - return titleView - } - .previewLayout(.fixed(width: 120, height: 24)) - UIViewPreview(width: 375) { - let titleView = HomeTimelineNavigationBarTitleView() - titleView.configure(state: .publishingPostLabel) - return titleView - } - .previewLayout(.fixed(width: 375, height: 44)) - UIViewPreview(width: 120) { - let titleView = HomeTimelineNavigationBarTitleView() - titleView.configure(state: .publishedButton) - return titleView - } - .previewLayout(.fixed(width: 120, height: 24)) - } - } - -} - -#endif - diff --git a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift deleted file mode 100644 index 9f736619b..000000000 --- a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// HomeTimelineNavigationBarTitleViewModel.swift -// Mastodon -// -// Created by sxiaojian on 2021/3/15. -// - -import Combine -import Foundation -import UIKit -import MastodonCore - -final class HomeTimelineNavigationBarTitleViewModel { - - static let offlineCounterLimit = 3 - - var disposeBag = Set() - private(set) var publishingProgressSubscription: AnyCancellable? - - // input - let context: AppContext - var networkErrorCount = CurrentValueSubject(0) - var networkErrorPublisher = PassthroughSubject() - - // output - let state = CurrentValueSubject(.logo) - let hasNewPosts = CurrentValueSubject(false) - let isOffline = CurrentValueSubject(false) - let isPublishingPost = CurrentValueSubject(false) - let isPublished = CurrentValueSubject(false) - let publishingProgress = PassthroughSubject() - - init(context: AppContext) { - self.context = context - - networkErrorPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.networkErrorCount.value = self.networkErrorCount.value + 1 - } - .store(in: &disposeBag) - - networkErrorCount - .receive(on: DispatchQueue.main) - .map { count in - return count >= HomeTimelineNavigationBarTitleViewModel.offlineCounterLimit - } - .assign(to: \.value, on: isOffline) - .store(in: &disposeBag) - - Publishers.CombineLatest( - context.publisherService.$statusPublishers, - context.publisherService.statusPublishResult.prepend(.failure(AppError.badRequest)) - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] statusPublishers, publishResult in - guard let self = self else { return } - - if statusPublishers.isEmpty { - self.isPublishingPost.value = false - self.isPublished.value = false - } else { - self.isPublishingPost.value = true - switch publishResult { - case .success: - self.isPublished.value = true - case .failure: - self.isPublished.value = false - } - } - } - .store(in: &disposeBag) - - Publishers.CombineLatest4( - hasNewPosts.eraseToAnyPublisher(), - isOffline.eraseToAnyPublisher(), - isPublishingPost.eraseToAnyPublisher(), - isPublished.eraseToAnyPublisher() - ) - .map { hasNewPosts, isOffline, isPublishingPost, isPublished -> State in - guard !isPublished else { return .publishedButton } - guard !isPublishingPost else { return .publishingPostLabel } - guard !isOffline else { return .offlineButton } - guard !hasNewPosts else { return .newPostButton } - return .logo - } - .receive(on: DispatchQueue.main) - .assign(to: \.value, on: state) - .store(in: &disposeBag) - -// state -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [weak self] state in -// guard let self = self else { return } -// switch state { -// case .publishingPostLabel: -// self.setupPublishingProgress() -// default: -// self.suspendPublishingProgress() -// } -// } -// .store(in: &disposeBag) - } -} - -extension HomeTimelineNavigationBarTitleViewModel { - // state order by priority from low to high - enum State: String { - case logo - case newPostButton - case offlineButton - case publishingPostLabel - case publishedButton - } -} - -// MARK: - New post state -extension HomeTimelineNavigationBarTitleViewModel { - - func newPostsIncoming() { - hasNewPosts.value = true - } - - private func resetNewPostState() { - hasNewPosts.value = false - } - -} - -// MARK: - Offline state -extension HomeTimelineNavigationBarTitleViewModel { - - func resetOfflineCounterListener() { - networkErrorCount.value = 0 - } - - func receiveLoadingStateCompletion(_ completion: Subscribers.Completion) { - switch completion { - case .failure: - networkErrorPublisher.send() - case .finished: - resetOfflineCounterListener() - } - } - - func handleScrollViewDidScroll(_ scrollView: UIScrollView) { - guard hasNewPosts.value else { return } - - let contentOffsetY = scrollView.contentOffset.y - let isScrollToTop = contentOffsetY < -scrollView.contentInset.top - guard isScrollToTop else { return } - resetNewPostState() - } - -} - -// MARK: Publish post state -//extension HomeTimelineNavigationBarTitleViewModel { -// -// func setupPublishingProgress() { -// let progressUpdatePublisher = Timer.publish(every: 0.016, on: .main, in: .common) // ~ 60FPS -// .autoconnect() -// .share() -// .eraseToAnyPublisher() -// -// publishingProgressSubscription = progressUpdatePublisher -// .map { _ in Float(0) } -// .scan(0.0) { progress, _ -> Float in -// return 0.95 * progress + 0.05 // progress + 0.05 * (1.0 - progress). ~ 1 sec to 0.95 (under 60FPS) -// } -// .subscribe(publishingProgress) -// } -// -// func suspendPublishingProgress() { -// publishingProgressSubscription = nil -// publishingProgress.send(0) -// } -// -//} - From 33653911c38615c0cc5a46bcded5a36c3b43267f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Apr 2024 17:06:54 +0200 Subject: [PATCH 02/12] Basic animation to show/hide :pill: (IOS-234) --- .../HomeTimelineViewController.swift | 66 ++++++++++++++++--- .../HomeTimeline/TimelineStatusPill.swift | 14 ++-- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index da8a1c747..8a7cf920a 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -100,6 +100,9 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media let refreshControl = RefreshControl() let timelinePill = TimelineStatusPill() + var timelinePillCenterXAnchor: NSLayoutConstraint? + var timelinePillVisibleTopAnchor: NSLayoutConstraint? + var timelinePillHiddenTopAnchor: NSLayoutConstraint? private func generateTimeSelectorMenu() -> UIMenu { @@ -299,17 +302,24 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) -// timelinePill.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(timelinePill) -// -// // has to up updated and animated -// timelinePill.update(with: .postSent) -// NSLayoutConstraint.activate([ -// timelinePill.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), -// timelinePill.centerXAnchor.constraint(equalTo: view.centerXAnchor), -// ]) + timelinePill.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(timelinePill) + + let timelinePillCenterXAnchor = timelinePill.centerXAnchor.constraint(equalTo: view.centerXAnchor) + let timelinePillVisibleTopAnchor = timelinePill.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8) + let timelinePillHiddenTopAnchor = view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: timelinePill.bottomAnchor, constant: 8) + + NSLayoutConstraint.activate([ + timelinePillHiddenTopAnchor, timelinePillCenterXAnchor + ]) + + timelinePill.addTarget(self, action: #selector(HomeTimelineViewController.timelinePillPressed(_:)), for: .touchUpInside) + + self.timelinePillCenterXAnchor = timelinePillCenterXAnchor + self.timelinePillVisibleTopAnchor = timelinePillVisibleTopAnchor + self.timelinePillHiddenTopAnchor = timelinePillHiddenTopAnchor } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -459,6 +469,42 @@ extension HomeTimelineViewController { } } + @objc private func timelinePillPressed(_ sender: TimelineStatusPill) { + guard let reason = sender.reason else { return } + switch reason { + case .newPosts: + print("Bring me to the new posts and disappear") + case .postSent: + print("Bring me to my post and disappear") + case .offline: + print("Just disappear") + } + + hideTimelinePill() + } + + private func showTimelinePill() { + guard let timelinePillHiddenTopAnchor, let timelinePillVisibleTopAnchor else { return } + + NSLayoutConstraint.deactivate([timelinePillHiddenTopAnchor]) + NSLayoutConstraint.activate([timelinePillVisibleTopAnchor]) + + UIView.animate(withDuration: 0.4) { + self.view.layoutIfNeeded() + } + } + + private func hideTimelinePill() { + guard let timelinePillHiddenTopAnchor, let timelinePillVisibleTopAnchor else { return } + + NSLayoutConstraint.deactivate([timelinePillVisibleTopAnchor]) + NSLayoutConstraint.activate([timelinePillHiddenTopAnchor]) + + UIView.animate(withDuration: 0.4, animations: { + self.view.layoutIfNeeded() + }) + } + } // MARK: - UIScrollViewDelegate extension HomeTimelineViewController { diff --git a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift index e24c512f9..1d24fe674 100644 --- a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift +++ b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift @@ -5,32 +5,34 @@ import MastodonAsset class TimelineStatusPill: UIButton { - func update(with state: State) { + var reason: Reason? + + func update(with reason: Reason) { + self.reason = reason var configuration = UIButton.Configuration.filled() - configuration.attributedTitle = AttributedString( - state.title, attributes: AttributeContainer( + reason.title, attributes: AttributeContainer( [ .font: UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)), .foregroundColor: UIColor.white ] )) - let image = state.image? + let image = reason.image? .withConfiguration(UIImage.SymbolConfiguration(paletteColors: [.white])) .withConfiguration(UIImage.SymbolConfiguration(textStyle: .subheadline)) .withConfiguration(UIImage.SymbolConfiguration(pointSize: 12, weight: .bold, scale: .medium)) configuration.image = image configuration.imagePadding = 8 - configuration.baseBackgroundColor = state.backgroundColor + configuration.baseBackgroundColor = reason.backgroundColor configuration.cornerStyle = .capsule self.configuration = configuration } - public enum State { + public enum Reason { case newPosts case postSent case offline From 985eb68766055b0af4463886947f61b8be6b302c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Apr 2024 18:35:51 +0200 Subject: [PATCH 03/12] Improve animations (IOS-234) --- .../HomeTimeline/HomeTimelineViewController.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 8a7cf920a..628c7ed3d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -486,11 +486,13 @@ extension HomeTimelineViewController { private func showTimelinePill() { guard let timelinePillHiddenTopAnchor, let timelinePillVisibleTopAnchor else { return } + timelinePill.alpha = 0 NSLayoutConstraint.deactivate([timelinePillHiddenTopAnchor]) NSLayoutConstraint.activate([timelinePillVisibleTopAnchor]) - UIView.animate(withDuration: 0.4) { - self.view.layoutIfNeeded() + UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.75, initialSpringVelocity: 0.9) { [weak self] in + self?.timelinePill.alpha = 1 + self?.view.layoutIfNeeded() } } @@ -499,9 +501,10 @@ extension HomeTimelineViewController { NSLayoutConstraint.deactivate([timelinePillVisibleTopAnchor]) NSLayoutConstraint.activate([timelinePillHiddenTopAnchor]) - - UIView.animate(withDuration: 0.4, animations: { - self.view.layoutIfNeeded() + timelinePill.alpha = 1 + UIView.animate(withDuration: 0.5, animations: { [weak self] in + self?.timelinePill.alpha = 0 + self?.view.layoutIfNeeded() }) } From 6f58c277c749c171f956cfe0524d2c00181cbbe7 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 7 Apr 2024 13:04:33 +0200 Subject: [PATCH 04/12] Fix initial layout (IOS-234) --- Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 628c7ed3d..605f016be 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -486,6 +486,8 @@ extension HomeTimelineViewController { private func showTimelinePill() { guard let timelinePillHiddenTopAnchor, let timelinePillVisibleTopAnchor else { return } + timelinePill.setNeedsLayout() + timelinePill.layoutIfNeeded() timelinePill.alpha = 0 NSLayoutConstraint.deactivate([timelinePillHiddenTopAnchor]) NSLayoutConstraint.activate([timelinePillVisibleTopAnchor]) From 527d2500245123e183a7b823de454f42b9ea1486 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 7 Apr 2024 13:53:13 +0200 Subject: [PATCH 05/12] Show "New Post :pill:" if new posts (IOS-234) --- .../HomeTimelineViewController.swift | 22 ++++++++++++++++--- ...omeTimelineViewModel+LoadLatestState.swift | 6 ++++- .../HomeTimeline/HomeTimelineViewModel.swift | 1 + .../HomeTimeline/TimelineStatusPill.swift | 1 - 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 605f016be..0ab223fd9 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -318,6 +318,20 @@ extension HomeTimelineViewController { self.timelinePillCenterXAnchor = timelinePillCenterXAnchor self.timelinePillVisibleTopAnchor = timelinePillVisibleTopAnchor self.timelinePillHiddenTopAnchor = timelinePillHiddenTopAnchor + + viewModel?.hasNewPosts + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] hasNewPosts in + guard let self else { return } + + if hasNewPosts { + self.timelinePill.update(with: .newPosts) + self.showTimelinePill() + } else { + self.hideTimelinePill() + } + }) + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { @@ -471,16 +485,18 @@ extension HomeTimelineViewController { @objc private func timelinePillPressed(_ sender: TimelineStatusPill) { guard let reason = sender.reason else { return } + switch reason { case .newPosts: - print("Bring me to the new posts and disappear") + scrollToTop(animated: true) + viewModel?.hasNewPosts.value = false case .postSent: print("Bring me to my post and disappear") case .offline: - print("Just disappear") + hideTimelinePill() } - hideTimelinePill() + } private func showTimelinePill() { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 97093660d..fa7045f46 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -159,7 +159,11 @@ extension HomeTimelineViewModel.LoadLatestState { await UIImpactFeedbackGenerator(style: .light) .impactOccurred() } - + + if newStatuses.isNotEmpty { + viewModel.hasNewPosts.value = true + } + } catch { await enter(state: Idle.self) viewModel.didLoadLatest.send() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index e934fe1d0..7daabe67e 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -33,6 +33,7 @@ final class HomeTimelineViewModel: NSObject { @Published var scrollPositionRecord: ScrollPositionRecord? = nil @Published var displaySettingBarButtonItem = true @Published var hasPendingStatusEditReload = false + var hasNewPosts = CurrentValueSubject(false) var timelineContext: MastodonFeed.Kind.TimelineContext = .home weak var tableView: UITableView? diff --git a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift index 1d24fe674..7846b8006 100644 --- a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift +++ b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift @@ -71,5 +71,4 @@ class TimelineStatusPill: UIButton { } } } - } From 6c0faede75bbaeecbc7b4dfd7041ea07b586c626 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 8 Apr 2024 10:40:16 +0200 Subject: [PATCH 06/12] =?UTF-8?q?Show=20"=E2=9C=85=20Post=20Sent!"-pill=20?= =?UTF-8?q?if=20post=20was=20sent=20successfully=20(IOS-234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maybe we could think about an error-message here, too? --- .../HomeTimeline/HomeTimelineViewController.swift | 14 ++++++++++++++ .../Scene/HomeTimeline/HomeTimelineViewModel.swift | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 0ab223fd9..e0891bcd6 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -332,6 +332,20 @@ extension HomeTimelineViewController { } }) .store(in: &disposeBag) + + context.publisherService.statusPublishResult.prepend(.failure(AppError.badRequest)) + .receive(on: DispatchQueue.main) + .sink { [weak self] publishResult in + guard let self else { return } + switch publishResult { + case .success: + self.timelinePill.update(with: .postSent) + self.showTimelinePill() + case .failure: + self.hideTimelinePill() + } + } + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 7daabe67e..dc8925b94 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -33,7 +33,7 @@ final class HomeTimelineViewModel: NSObject { @Published var scrollPositionRecord: ScrollPositionRecord? = nil @Published var displaySettingBarButtonItem = true @Published var hasPendingStatusEditReload = false - var hasNewPosts = CurrentValueSubject(false) + let hasNewPosts = CurrentValueSubject(false) var timelineContext: MastodonFeed.Kind.TimelineContext = .home weak var tableView: UITableView? From 8884b326fc03ac7adea7e5a0def564c7cfb130dc Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Apr 2024 16:12:32 +0200 Subject: [PATCH 07/12] Re-enable offlineness-hint (IOS-234) --- .../HomeTimelineViewController.swift | 14 ++++++++++++ ...omeTimelineViewModel+LoadLatestState.swift | 2 ++ ...omeTimelineViewModel+LoadOldestState.swift | 3 +++ .../HomeTimeline/HomeTimelineViewModel.swift | 22 +++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index e0891bcd6..844053fcf 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -333,6 +333,20 @@ extension HomeTimelineViewController { }) .store(in: &disposeBag) + viewModel?.isOffline + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] isOffline in + guard let self else { return } + + if isOffline { + self.timelinePill.update(with: .offline) + self.showTimelinePill() + } else { + self.hideTimelinePill() + } + }) + .store(in: &disposeBag) + context.publisherService.statusPublishResult.prepend(.failure(AppError.badRequest)) .receive(on: DispatchQueue.main) .sink { [weak self] publishResult in diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index fa7045f46..78282fa8f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -129,6 +129,7 @@ extension HomeTimelineViewModel.LoadLatestState { } await enter(state: Idle.self) + viewModel.receiveLoadingStateCompletion(.finished) // stop refresher if no new statuses let statuses = response.value @@ -167,6 +168,7 @@ extension HomeTimelineViewModel.LoadLatestState { } catch { await enter(state: Idle.self) viewModel.didLoadLatest.send() + viewModel.receiveLoadingStateCompletion(.failure(error)) } } // end Task } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 4436c9b79..565e7c8b0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -84,8 +84,11 @@ extension HomeTimelineViewModel.LoadOldestState { await self.enter(state: Idle.self) } + viewModel.receiveLoadingStateCompletion(.finished) + } catch { await self.enter(state: Fail.self) + viewModel.receiveLoadingStateCompletion(.failure(error)) } } // end Task } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index dc8925b94..e19913873 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -34,6 +34,11 @@ final class HomeTimelineViewModel: NSObject { @Published var displaySettingBarButtonItem = true @Published var hasPendingStatusEditReload = false let hasNewPosts = CurrentValueSubject(false) + + /// Becomes `true` if `networkErrorCount` is bigger than 5 + let isOffline = CurrentValueSubject(false) + var networkErrorCount = CurrentValueSubject(0) + var timelineContext: MastodonFeed.Kind.TimelineContext = .home weak var tableView: UITableView? @@ -103,8 +108,25 @@ final class HomeTimelineViewModel: NSObject { }) .store(in: &disposeBag) + networkErrorCount + .receive(on: DispatchQueue.main) + .map { errorCount in + return errorCount >= 5 + } + .assign(to: \.value, on: isOffline) + .store(in: &disposeBag) + self.dataController.loadInitial(kind: .home(timeline: timelineContext)) } + + func receiveLoadingStateCompletion(_ completion: Subscribers.Completion) { + switch completion { + case .failure: + networkErrorCount.value = networkErrorCount.value + 1 + case .finished: + networkErrorCount.value = 0 + } + } } extension HomeTimelineViewModel { From 6e61e3ca25ef9d30b29fcc00ffc3502d1cb24058 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Apr 2024 17:23:09 +0200 Subject: [PATCH 08/12] Scale pill when pressing (IOS-234) --- .../HomeTimelineViewController.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 844053fcf..742843476 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -313,7 +313,9 @@ extension HomeTimelineViewController { timelinePillHiddenTopAnchor, timelinePillCenterXAnchor ]) - timelinePill.addTarget(self, action: #selector(HomeTimelineViewController.timelinePillPressed(_:)), for: .touchUpInside) + timelinePill.addTarget(self, action: #selector(HomeTimelineViewController.timelinePillTouched(_:)), for: .touchDown) + timelinePill.addTarget(self, action: #selector(HomeTimelineViewController.timelinePillPressedInside(_:)), for: .touchUpInside) + timelinePill.addTarget(self, action: #selector(HomeTimelineViewController.timelinePillTouchedOutside(_:)), for: .touchUpOutside) self.timelinePillCenterXAnchor = timelinePillCenterXAnchor self.timelinePillVisibleTopAnchor = timelinePillVisibleTopAnchor @@ -511,9 +513,25 @@ extension HomeTimelineViewController { } } - @objc private func timelinePillPressed(_ sender: TimelineStatusPill) { + @objc private func timelinePillTouched(_ sender: TimelineStatusPill) { + UIView.animate(withDuration: 0.05) { + sender.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95) + } + } + + @objc private func timelinePillTouchedOutside(_ sender: TimelineStatusPill) { + UIView.animate(withDuration: 0.05) { + sender.transform = CGAffineTransform.identity.scaledBy(x: 100/95.0, y: 100/95.0) + } + } + + @objc private func timelinePillPressedInside(_ sender: TimelineStatusPill) { guard let reason = sender.reason else { return } + UIView.animate(withDuration: 0.05) { + sender.transform = CGAffineTransform.identity.scaledBy(x: 100/95.0, y: 100/95.0) + } + switch reason { case .newPosts: scrollToTop(animated: true) @@ -523,8 +541,6 @@ extension HomeTimelineViewController { case .offline: hideTimelinePill() } - - } private func showTimelinePill() { From c1c6ef44e970504a0ec2d5901b63b9be414f3464 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Apr 2024 18:44:42 +0200 Subject: [PATCH 09/12] Add some :sparkles: styling (IOS-234) --- .../HomeTimeline/TimelineStatusPill.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift index 7846b8006..df85d6ca5 100644 --- a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift +++ b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift @@ -15,7 +15,6 @@ class TimelineStatusPill: UIButton { reason.title, attributes: AttributeContainer( [ .font: UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)), - .foregroundColor: UIColor.white ] )) @@ -26,12 +25,28 @@ class TimelineStatusPill: UIButton { configuration.image = image configuration.imagePadding = 8 - configuration.baseBackgroundColor = reason.backgroundColor configuration.cornerStyle = .capsule + configuration.background.backgroundColor = reason.backgroundColor self.configuration = configuration } + override func updateConfiguration() { + guard let reason, var updatedConfiguration = configuration else { + return super.updateConfiguration() + } + + switch state { + case .selected, .highlighted, .focused: + updatedConfiguration.baseForegroundColor = UIColor.white.withAlphaComponent(0.5) + default: + updatedConfiguration.baseForegroundColor = .white + } + + updatedConfiguration.background.backgroundColor = reason.backgroundColor + self.configuration = updatedConfiguration + } + public enum Reason { case newPosts case postSent From bef0536e729c0d1122a97e0066594b9c5a0487af Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Apr 2024 19:24:15 +0200 Subject: [PATCH 10/12] add a little shadow (IOS-234) --- Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift index df85d6ca5..1e10322b5 100644 --- a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift +++ b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift @@ -29,6 +29,11 @@ class TimelineStatusPill: UIButton { configuration.background.backgroundColor = reason.backgroundColor self.configuration = configuration + + layer.shadowColor = reason.backgroundColor.cgColor + layer.shadowOpacity = 0.15 + layer.shadowOffset = .init(width: 0, height: 8) + layer.shadowRadius = 8 } override func updateConfiguration() { From 8bb9bca4b93961696b22c0738b13d9d1252a6929 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 10 Apr 2024 14:03:13 +0200 Subject: [PATCH 11/12] Sprinkle in localization (IOS-234) --- .../input/Base.lproj/app.json | 11 +++------ Localization/app.json | 11 +++------ .../HomeTimeline/TimelineStatusPill.swift | 8 +++---- .../Generated/Strings.swift | 24 +++++++------------ .../Resources/Base.lproj/Localizable.strings | 9 +++---- 5 files changed, 21 insertions(+), 42 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 8658e52d8..261d40e95 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -474,15 +474,10 @@ "following": "Following", "local_community": "Local" }, - "navigation_bar_state": { + "timeline_pill": { "offline": "Offline", - "new_posts": "See new posts", - "published": "Published!", - "Publishing": "Publishing post...", - "accessibility": { - "logo_label": "Mastodon", - "logo_hint": "Tap to scroll to top and tap again to previous location" - } + "new_posts": "New Posts", + "post_sent": "Post Sent" } }, "suggestion_account": { diff --git a/Localization/app.json b/Localization/app.json index 8658e52d8..261d40e95 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -474,15 +474,10 @@ "following": "Following", "local_community": "Local" }, - "navigation_bar_state": { + "timeline_pill": { "offline": "Offline", - "new_posts": "See new posts", - "published": "Published!", - "Publishing": "Publishing post...", - "accessibility": { - "logo_label": "Mastodon", - "logo_hint": "Tap to scroll to top and tap again to previous location" - } + "new_posts": "New Posts", + "post_sent": "Post Sent" } }, "suggestion_account": { diff --git a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift index 1e10322b5..2000d24d2 100644 --- a/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift +++ b/Mastodon/Scene/HomeTimeline/TimelineStatusPill.swift @@ -2,6 +2,7 @@ import UIKit import MastodonAsset +import MastodonLocalization class TimelineStatusPill: UIButton { @@ -80,14 +81,13 @@ class TimelineStatusPill: UIButton { } var title: String { - //TODO: Localization switch self { case .newPosts: - return "New Posts" + return L10n.Scene.HomeTimeline.TimelinePill.newPosts case .postSent: - return "Post Sent" + return L10n.Scene.HomeTimeline.TimelinePill.postSent case .offline: - return "Offline" + return L10n.Scene.HomeTimeline.TimelinePill.offline } } } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 6fddef9d2..fcd63dc9f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -843,28 +843,20 @@ public enum L10n { public enum HomeTimeline { /// Home public static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title", fallback: "Home") - public enum NavigationBarState { - /// See new posts - public static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.NewPosts", fallback: "See new posts") - /// Offline - public static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Offline", fallback: "Offline") - /// Published! - public static let published = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Published", fallback: "Published!") - /// Publishing post... - public static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing", fallback: "Publishing post...") - public enum Accessibility { - /// Tap to scroll to top and tap again to previous location - public static let logoHint = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint", fallback: "Tap to scroll to top and tap again to previous location") - /// Mastodon - public static let logoLabel = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel", fallback: "Mastodon") - } - } public enum TimelineMenu { /// Following public static let following = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Following", fallback: "Following") /// Local public static let localCommunity = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.LocalCommunity", fallback: "Local") } + public enum TimelinePill { + /// New Posts + public static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.TimelinePill.NewPosts", fallback: "New Posts") + /// Offline + public static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.TimelinePill.Offline", fallback: "Offline") + /// Post Sent + public static let postSent = L10n.tr("Localizable", "Scene.HomeTimeline.TimelinePill.PostSent", fallback: "Post Sent") + } } public enum Login { /// Log you in on the server you created your account on. diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 60fb82e8f..9c6b106d2 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -298,14 +298,11 @@ uploaded to Mastodon."; "Scene.Follower.Title" = "follower"; "Scene.Following.Footer" = "Follows from other servers are not displayed."; "Scene.Following.Title" = "following"; -"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tap to scroll to top and tap again to previous location"; -"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Mastodon"; -"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts"; -"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline"; -"Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; -"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; "Scene.HomeTimeline.TimelineMenu.Following" = "Following"; "Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local"; +"Scene.HomeTimeline.TimelinePill.NewPosts" = "New Posts"; +"Scene.HomeTimeline.TimelinePill.Offline" = "Offline"; +"Scene.HomeTimeline.TimelinePill.PostSent" = "Post Sent"; "Scene.HomeTimeline.Title" = "Home"; "Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; "Scene.Login.Subtitle" = "Log you in on the server you created your account on."; From 0b282c44ad6b5f3d25d645b24a08318d36806b5b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 10 Apr 2024 14:47:48 +0200 Subject: [PATCH 12/12] Go to top when post was sent (IOS-234) --- Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 742843476..8559025d4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -537,7 +537,8 @@ extension HomeTimelineViewController { scrollToTop(animated: true) viewModel?.hasNewPosts.value = false case .postSent: - print("Bring me to my post and disappear") + scrollToTop(animated: true) + hideTimelinePill() case .offline: hideTimelinePill() }