Improve empty state for lists (IOS-287)

This commit is contained in:
Marcus Kida 2024-07-24 15:32:32 +02:00
parent 50ee1c51e4
commit 07b0ddc14f
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
6 changed files with 101 additions and 59 deletions

View File

@ -491,6 +491,9 @@
"offline": "Offline", "offline": "Offline",
"new_posts": "New Posts", "new_posts": "New Posts",
"post_sent": "Post Sent" "post_sent": "Post Sent"
},
"empty_state": {
"list_empty_message_title": "This list is empty"
} }
}, },
"suggestion_account": { "suggestion_account": {

View File

@ -29,6 +29,10 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
let mediaPreviewTransitionController = MediaPreviewTransitionController() let mediaPreviewTransitionController = MediaPreviewTransitionController()
enum EmptyViewUseCase {
case timeline, list
}
let friendsAssetImageView: UIImageView = { let friendsAssetImageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.image = Asset.Asset.friends.image imageView.image = Asset.Asset.friends.image
@ -335,24 +339,24 @@ extension HomeTimelineViewController {
viewModel?.timelineIsEmpty viewModel?.timelineIsEmpty
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] isEmpty in .sink { [weak self] state in
if isEmpty { guard let state else {
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 {
self?.emptyView.removeFromSuperview() 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) .store(in: &disposeBag)
@ -478,7 +482,7 @@ extension HomeTimelineViewController {
} }
extension HomeTimelineViewController { extension HomeTimelineViewController {
func showEmptyView() { func showEmptyView(_ state: HomeTimelineViewModel.EmptyViewState) {
if emptyView.superview != nil { if emptyView.superview != nil {
return return
} }
@ -494,48 +498,61 @@ extension HomeTimelineViewController {
if emptyView.arrangedSubviews.count > 0 { if emptyView.arrangedSubviews.count > 0 {
return 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 = { switch state {
let button = HighlightDimmableButton() case .list:
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)) let noStatusesLabel: UILabel = {
button.setTitle(L10n.Common.Controls.Actions.manuallySearch, for: .normal) let label = UILabel()
button.setTitleColor(Asset.Colors.Brand.blurple.color, for: .normal) label.text = L10n.Scene.HomeTimeline.EmptyState.listEmptyMessageTitle
button.addTarget(self, action: #selector(HomeTimelineViewController.manuallySearchButtonPressed(_:)), for: .touchUpInside) label.textColor = Asset.Colors.Label.secondary.color
return button 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 topPaddingView = UIView() let manuallySearchButton: HighlightDimmableButton = {
let bottomPaddingView = UIView() 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) let topPaddingView = UIView()
emptyView.addArrangedSubview(friendsAssetImageView) let bottomPaddingView = UIView()
emptyView.addArrangedSubview(bottomPaddingView)
topPaddingView.translatesAutoresizingMaskIntoConstraints = false emptyView.addArrangedSubview(topPaddingView)
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false emptyView.addArrangedSubview(friendsAssetImageView)
NSLayoutConstraint.activate([ emptyView.addArrangedSubview(bottomPaddingView)
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
])
let buttonContainerStackView = UIStackView() topPaddingView.translatesAutoresizingMaskIntoConstraints = false
emptyView.addArrangedSubview(buttonContainerStackView) bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
buttonContainerStackView.isLayoutMarginsRelativeArrangement = true NSLayoutConstraint.activate([
buttonContainerStackView.layoutMargins = UIEdgeInsets(top: 0, left: 32, bottom: 22, right: 32) topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
buttonContainerStackView.axis = .vertical manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
buttonContainerStackView.spacing = 17 ])
buttonContainerStackView.addArrangedSubview(findPeopleButton) let buttonContainerStackView = UIStackView()
buttonContainerStackView.addArrangedSubview(manuallySearchButton) 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)
}
} }
} }

View File

@ -105,6 +105,7 @@ extension HomeTimelineViewModel.LoadLatestState {
guard let viewModel else { return } guard let viewModel else { return }
viewModel.timelineIsEmpty.send(nil)
Task { @MainActor in Task { @MainActor in
let latestFeedRecords = viewModel.dataController.records let latestFeedRecords = viewModel.dataController.records
@ -170,7 +171,19 @@ extension HomeTimelineViewModel.LoadLatestState {
viewModel.dataController.records = (toAdd + latestFeedRecords).removingDuplicates() 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 { if !isUserInitiated {
FeedbackGenerator.shared.generate(.impact(.light)) FeedbackGenerator.shared.generate(.impact(.light))

View File

@ -44,10 +44,14 @@ final class HomeTimelineViewModel: NSObject {
} }
} }
enum EmptyViewState {
case timeline, list
}
weak var tableView: UITableView? weak var tableView: UITableView?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
let timelineIsEmpty = CurrentValueSubject<Bool, Never>(false) let timelineIsEmpty = CurrentValueSubject<EmptyViewState?, Never>(nil)
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>() let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()
// output // output

View File

@ -857,6 +857,10 @@ public enum L10n {
public enum HomeTimeline { public enum HomeTimeline {
/// Home /// Home
public static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title", fallback: "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 { public enum TimelineMenu {
/// Following /// Following
public static let following = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Following", fallback: "Following") public static let following = L10n.tr("Localizable", "Scene.HomeTimeline.TimelineMenu.Following", fallback: "Following")

View File

@ -303,6 +303,7 @@ uploaded to Mastodon.";
"Scene.Follower.Title" = "follower"; "Scene.Follower.Title" = "follower";
"Scene.Following.Footer" = "Follows from other servers are not displayed."; "Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.Following.Title" = "following"; "Scene.Following.Title" = "following";
"Scene.HomeTimeline.EmptyState.ListEmptyMessageTitle" = "This list is empty";
"Scene.HomeTimeline.TimelineMenu.Following" = "Following"; "Scene.HomeTimeline.TimelineMenu.Following" = "Following";
"Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local"; "Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local";
"Scene.HomeTimeline.TimelineMenu.Lists.Title" = "Lists"; "Scene.HomeTimeline.TimelineMenu.Lists.Title" = "Lists";