Improve empty state for lists (IOS-287)
This commit is contained in:
parent
50ee1c51e4
commit
07b0ddc14f
|
@ -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": {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in New Issue