Skip to content

Commit

Permalink
Fixes for empty list state and hashtags (IOS-287) (#1328)
Browse files Browse the repository at this point in the history
* Adds missing #-symbol in Hashtag Widgets and Timeline Views
* Removes activity indicator in case loading ended on timeline (e.g.
when loading list contents)
* Adds different empty view for lists
  • Loading branch information
kimar authored Jul 24, 2024
2 parents cd4ef9f + 07b0ddc commit aa7b6ff
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 68 deletions.
3 changes: 3 additions & 0 deletions Localization/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
138 changes: 78 additions & 60 deletions Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -202,12 +206,13 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
).singleOutput().value) ?? []

var listEntries = lists.map { entry in
return LabeledAction(title: entry.name, image: nil, handler: { [weak self] in
let entryName = "#\(entry.name)"
return LabeledAction(title: entryName, image: nil, handler: { [weak self] in
guard let self, let viewModel = self.viewModel else { return }
viewModel.timelineContext = .hashtag(entry.name)
viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.ContextSwitch.self)
timelineSelectorButton.setAttributedTitle(
.init(string: entry.name, attributes: [
.init(string: entryName, attributes: [
.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
]),
for: .normal)
Expand Down Expand Up @@ -334,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
}
.sink { [weak self] state in
guard let state else {
self?.emptyView.removeFromSuperview()
return
}
self?.showEmptyView(state)

if (self?.viewModel?.presentedSuggestions == false) && userDoesntFollowPeople {
self?.findPeopleButtonPressed(self)
self?.viewModel?.presentedSuggestions = true
}
let userDoesntFollowPeople: Bool
if let authContext = self?.authContext,
let me = authContext.mastodonAuthenticationBox.authentication.account() {
userDoesntFollowPeople = me.followersCount == 0
} else {
self?.emptyView.removeFromSuperview()
userDoesntFollowPeople = true
}

if (self?.viewModel?.presentedSuggestions == false) && userDoesntFollowPeople {
self?.findPeopleButtonPressed(self)
self?.viewModel?.presentedSuggestions = true
}
}
.store(in: &disposeBag)
Expand Down Expand Up @@ -477,7 +482,7 @@ extension HomeTimelineViewController {
}

extension HomeTimelineViewController {
func showEmptyView() {
func showEmptyView(_ state: HomeTimelineViewModel.EmptyViewState) {
if emptyView.superview != nil {
return
}
Expand All @@ -493,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()

emptyView.addArrangedSubview(topPaddingView)
emptyView.addArrangedSubview(friendsAssetImageView)
emptyView.addArrangedSubview(bottomPaddingView)

topPaddingView.translatesAutoresizingMaskIntoConstraints = false
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
])

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)
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
}()

let topPaddingView = UIView()
let bottomPaddingView = UIView()

emptyView.addArrangedSubview(topPaddingView)
emptyView.addArrangedSubview(friendsAssetImageView)
emptyView.addArrangedSubview(bottomPaddingView)

topPaddingView.translatesAutoresizingMaskIntoConstraints = false
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
])

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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ extension HomeTimelineViewModel.LoadLatestState {

guard let viewModel else { return }


viewModel.timelineIsEmpty.send(nil)

Task { @MainActor in
let latestFeedRecords = viewModel.dataController.records

Expand Down Expand Up @@ -152,8 +153,9 @@ extension HomeTimelineViewModel.LoadLatestState {

if statuses.isEmpty {
// stop refresher if no new statuses
viewModel.dataController.records = []
viewModel.didLoadLatest.send()
} else {
} else {
var toAdd = [MastodonFeed]()

let last = statuses.last
Expand All @@ -169,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))
}
Expand Down
6 changes: 5 additions & 1 deletion Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool, Never>(false)
let timelineIsEmpty = CurrentValueSubject<EmptyViewState?, Never>(nil)
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()

// output
Expand Down
4 changes: 2 additions & 2 deletions MastodonIntent/Handler/HashtagIntentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class HashtagIntentHandler: INExtension, HashtagIntentHandling {
.search(query: .init(q: searchTerm, type: .hashtags), authenticationBox: authenticationBox)
.value
.hashtags
.compactMap { $0.name as NSString }
.compactMap { "#\($0.name)" as NSString }

results = searchResults

Expand All @@ -33,7 +33,7 @@ class HashtagIntentHandler: INExtension, HashtagIntentHandling {
query: Mastodon.API.Account.FollowedTagsQuery(limit: nil),
authenticationBox: authenticationBox)
.value
.compactMap { $0.name as NSString }
.compactMap { "#\($0.name)" as NSString }

results = followedTags

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion WidgetExtension/Variants/Hashtag/HashtagWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ extension HashtagWidgetProvider {
let desiredHashtag: String

if let hashtag = configuration.hashtag {
desiredHashtag = hashtag
desiredHashtag = hashtag.replacingOccurrences(of: "#", with: "")
} else {
return completion(.notFound("hashtag"))
}
Expand Down

0 comments on commit aa7b6ff

Please sign in to comment.