From 07b0ddc14f4aa67054da79a67ec1b3ca761405d2 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 24 Jul 2024 15:32:32 +0200 Subject: [PATCH] Improve empty state for lists (IOS-287) --- Localization/app.json | 3 + .../HomeTimelineViewController.swift | 127 ++++++++++-------- ...omeTimelineViewModel+LoadLatestState.swift | 19 ++- .../HomeTimeline/HomeTimelineViewModel.swift | 6 +- .../Generated/Strings.swift | 4 + .../Resources/Base.lproj/Localizable.strings | 1 + 6 files changed, 101 insertions(+), 59 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index c84499439..a9dc4f039 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -491,6 +491,9 @@ "offline": "Offline", "new_posts": "New Posts", "post_sent": "Post Sent" + }, + "empty_state": { + "list_empty_message_title": "This list is empty" } }, "suggestion_account": { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index a8b94c12e..cfe79afa4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -28,6 +28,10 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media var viewModel: HomeTimelineViewModel? let mediaPreviewTransitionController = MediaPreviewTransitionController() + + enum EmptyViewUseCase { + case timeline, list + } let friendsAssetImageView: UIImageView = { let imageView = UIImageView() @@ -335,24 +339,24 @@ extension HomeTimelineViewController { viewModel?.timelineIsEmpty .receive(on: DispatchQueue.main) - .sink { [weak self] isEmpty in - if isEmpty { - self?.showEmptyView() - - let userDoesntFollowPeople: Bool - if let authContext = self?.authContext, - let me = authContext.mastodonAuthenticationBox.authentication.account() { - userDoesntFollowPeople = me.followersCount == 0 - } else { - userDoesntFollowPeople = true - } - - if (self?.viewModel?.presentedSuggestions == false) && userDoesntFollowPeople { - self?.findPeopleButtonPressed(self) - self?.viewModel?.presentedSuggestions = true - } - } else { + .sink { [weak self] state in + guard let state else { self?.emptyView.removeFromSuperview() + return + } + self?.showEmptyView(state) + + let userDoesntFollowPeople: Bool + if let authContext = self?.authContext, + let me = authContext.mastodonAuthenticationBox.authentication.account() { + userDoesntFollowPeople = me.followersCount == 0 + } else { + userDoesntFollowPeople = true + } + + if (self?.viewModel?.presentedSuggestions == false) && userDoesntFollowPeople { + self?.findPeopleButtonPressed(self) + self?.viewModel?.presentedSuggestions = true } } .store(in: &disposeBag) @@ -478,7 +482,7 @@ extension HomeTimelineViewController { } extension HomeTimelineViewController { - func showEmptyView() { + func showEmptyView(_ state: HomeTimelineViewModel.EmptyViewState) { if emptyView.superview != nil { return } @@ -494,48 +498,61 @@ extension HomeTimelineViewController { 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.blurple.color, for: .normal) - button.addTarget(self, action: #selector(HomeTimelineViewController.manuallySearchButtonPressed(_:)), for: .touchUpInside) - return button - }() - let topPaddingView = UIView() - let bottomPaddingView = UIView() + switch state { + case .list: + let noStatusesLabel: UILabel = { + let label = UILabel() + label.text = L10n.Scene.HomeTimeline.EmptyState.listEmptyMessageTitle + label.textColor = Asset.Colors.Label.secondary.color + label.textAlignment = .center + return label + }() + emptyView.addArrangedSubview(noStatusesLabel) + case .timeline: + 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.blurple.color, for: .normal) + button.addTarget(self, action: #selector(HomeTimelineViewController.manuallySearchButtonPressed(_:)), for: .touchUpInside) + return button + }() - emptyView.addArrangedSubview(topPaddingView) - emptyView.addArrangedSubview(friendsAssetImageView) - emptyView.addArrangedSubview(bottomPaddingView) + let topPaddingView = UIView() + let bottomPaddingView = UIView() - topPaddingView.translatesAutoresizingMaskIntoConstraints = false - bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8), - manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), - ]) + emptyView.addArrangedSubview(topPaddingView) + emptyView.addArrangedSubview(friendsAssetImageView) + emptyView.addArrangedSubview(bottomPaddingView) - 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 + topPaddingView.translatesAutoresizingMaskIntoConstraints = false + bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8), + manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) - buttonContainerStackView.addArrangedSubview(findPeopleButton) - buttonContainerStackView.addArrangedSubview(manuallySearchButton) + 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) + } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 74f313e11..025188aeb 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -105,7 +105,8 @@ extension HomeTimelineViewModel.LoadLatestState { guard let viewModel else { return } - + viewModel.timelineIsEmpty.send(nil) + Task { @MainActor in let latestFeedRecords = viewModel.dataController.records @@ -170,8 +171,20 @@ extension HomeTimelineViewModel.LoadLatestState { viewModel.dataController.records = (toAdd + latestFeedRecords).removingDuplicates() } - viewModel.timelineIsEmpty.value = latestStatusIDs.isEmpty && statuses.isEmpty - + + viewModel.timelineIsEmpty.value = (latestStatusIDs.isEmpty && statuses.isEmpty) ? { + switch viewModel.timelineContext { + case .home: + return .timeline + case .public: + return .timeline + case .list: + return .list + case .hashtag: + return .list + } + }() : nil + if !isUserInitiated { FeedbackGenerator.shared.generate(.impact(.light)) } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 1c3f81cae..297c43be0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -43,11 +43,15 @@ final class HomeTimelineViewModel: NSObject { hasNewPosts.send(false) } } + + enum EmptyViewState { + case timeline, list + } weak var tableView: UITableView? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? - let timelineIsEmpty = CurrentValueSubject(false) + let timelineIsEmpty = CurrentValueSubject(nil) let homeTimelineNeedRefresh = PassthroughSubject() // output diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 3a8787b50..ca4c56c63 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -857,6 +857,10 @@ public enum L10n { public enum HomeTimeline { /// Home public static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title", fallback: "Home") + public enum EmptyState { + /// This list is empty + public static let listEmptyMessageTitle = L10n.tr("Localizable", "Scene.HomeTimeline.EmptyState.ListEmptyMessageTitle", fallback: "This list is empty") + } public enum TimelineMenu { /// Following public static let following = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Following", fallback: "Following") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 5a6db9ba3..7e79a9420 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -303,6 +303,7 @@ uploaded to Mastodon."; "Scene.Follower.Title" = "follower"; "Scene.Following.Footer" = "Follows from other servers are not displayed."; "Scene.Following.Title" = "following"; +"Scene.HomeTimeline.EmptyState.ListEmptyMessageTitle" = "This list is empty"; "Scene.HomeTimeline.TimelineMenu.Following" = "Following"; "Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local"; "Scene.HomeTimeline.TimelineMenu.Lists.Title" = "Lists";