Show up to three hashtags and up to three users (IOS-141)

This commit is contained in:
Nathan Mattes 2023-09-16 17:57:29 +02:00
parent 2e384f3cb5
commit ed11d01267
5 changed files with 87 additions and 62 deletions

View File

@ -87,11 +87,8 @@ extension HomeTimelineViewModel {
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
if !hasChanges && !self.hasPendingStatusEditReload {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes")
self.didLoadLatest.send()
return
} else {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes")
}
guard let difference = self.calculateReloadSnapshotDifference(
@ -101,7 +98,6 @@ extension HomeTimelineViewModel {
) else {
self.updateSnapshotUsingReloadData(snapshot: newSnapshot)
self.didLoadLatest.send()
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot")
return
}
@ -111,7 +107,6 @@ extension HomeTimelineViewModel {
contentOffset.y = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge
tableView.setContentOffset(contentOffset, animated: false)
self.didLoadLatest.send()
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot")
self.hasPendingStatusEditReload = false
} // end Task
}

View File

@ -48,24 +48,37 @@ enum SearchResultOverviewItem: Hashable {
}
enum SuggestionSectionEntry: Hashable {
//TODO: Use User instead
case hashtag(tag: Mastodon.Entity.Tag)
case profile(ManagedObjectRecord<MastodonUser>)
case profile(user: Mastodon.Entity.Account)
var title: String? {
if case let .hashtag(tag) = self {
return tag.name
} else {
return nil
switch self {
case .hashtag(tag: let tag):
return tag.name
case .profile(user: let user):
return "\(user.displayName)\(user.acct)"
}
// if case let .hashtag(tag) = self {
// return tag.name
// } else {
// return nil
// }
}
var icon: UIImage? {
if case let .hashtag(tag) = self {
return UIImage(systemName: "number")
} else {
return nil
switch self {
case .hashtag(tag: _):
return UIImage(systemName: "number")
case .profile(user: _):
return UIImage(systemName: "person.circle")
}
// if case let .hashtag(tag) = self {
// } else {
// }
}
}
}

View File

@ -23,6 +23,7 @@ class SearchResultsOverviewTableViewController: UIViewController {
var dataSource: UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>?
weak var delegate: SearchResultsOverviewTableViewControllerDeleagte?
var activeTask: Task<Void, Never>?
init(appContext: AppContext, authContext: AuthContext) {
@ -48,22 +49,29 @@ class SearchResultsOverviewTableViewController: UIViewController {
return cell
case .suggestion(let suggestion):
switch suggestion {
case .hashtag(let hashtag):
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
cell.configure(item: .hashtag(tag: hashtag))
return cell
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
case .profile(let profile):
guard let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as? UserTableViewCell else { fatalError() }
// cell.configure(me: <#T##MastodonUser?#>, tableView: <#T##UITableView#>, viewModel: <#T##UserTableViewCell.ViewModel#>, delegate: <#T##UserTableViewCellDelegate?#>)
return cell
}
cell.configure(item: suggestion)
return cell
// switch suggestion {
//
// case .hashtag(let hashtag):
// guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
//
// cell.configure(item: .hashtag(tag: hashtag))
// return cell
//
// case .profile(let profile):
// //TODO: Use `UserFetchedResultsController` or `Persistence.MastodonUser.fetch` ???
// guard let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as? UserTableViewCell else { fatalError() }
//
//// cell.configure(me: <#T##MastodonUser?#>, tableView: <#T##UITableView#>, viewModel: <#T##UserTableViewCell.ViewModel#>, delegate: <#T##UserTableViewCellDelegate?#>)
//
// return cell
// }
}
}
@ -79,9 +87,20 @@ class SearchResultsOverviewTableViewController: UIViewController {
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func showStandardSearch(for searchText: String) {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var snapshot = NSDiffableDataSourceSnapshot<SearchResultOverviewSection, SearchResultOverviewItem>()
snapshot.appendSections([.default, .suggestions])
dataSource?.apply(snapshot, animatingDifferences: false)
}
func showStandardSearch(for searchText: String) {
guard let dataSource else { return }
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .default))
snapshot.appendItems([.default(.posts(searchText)),
.default(.people(searchText)),
.default(.profile(searchText, authContext.mastodonAuthenticationBox.domain))], toSection: .default)
@ -90,18 +109,29 @@ class SearchResultsOverviewTableViewController: UIViewController {
//TODO: Check if Mastodon-URL
snapshot.appendItems([.default(.openLink(searchText))], toSection: .default)
}
dataSource?.apply(snapshot, animatingDifferences: false)
dataSource.apply(snapshot, animatingDifferences: false)
}
func searchForSuggestions(for searchText: String) {
activeTask?.cancel()
guard let dataSource else { return }
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .suggestions))
dataSource.apply(snapshot, animatingDifferences: false)
guard searchText.isNotEmpty else { return }
let query = Mastodon.API.V2.Search.Query(
q: searchText,
type: .default,
resolve: true
)
Task {
let searchTask = Task {
do {
let searchResult = try await appContext.apiService.search(
query: query,
@ -111,22 +141,29 @@ class SearchResultsOverviewTableViewController: UIViewController {
let firstThreeHashtags = searchResult.hashtags.prefix(3)
let firstThreeUsers = searchResult.accounts.prefix(3)
guard var snapshot = dataSource?.snapshot() else { return }
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .suggestions))
snapshot.appendItems(firstThreeHashtags.map { .suggestion(.hashtag(tag: $0)) }, toSection: .suggestions )
// snapshot.appendItems(firstThreeUsers.map { .suggestion(.profile($0.displayName)) }, toSection: .suggestions )
await MainActor.run {
dataSource?.apply(snapshot, animatingDifferences: false)
if firstThreeHashtags.isNotEmpty {
snapshot.appendItems(firstThreeHashtags.map { .suggestion(.hashtag(tag: $0)) }, toSection: .suggestions )
}
if firstThreeUsers.isNotEmpty {
snapshot.appendItems(firstThreeUsers.map { .suggestion(.profile(user: $0)) }, toSection: .suggestions )
}
guard Task.isCancelled == false else { return }
await MainActor.run {
dataSource.apply(snapshot, animatingDifferences: false)
}
} catch {
// do nothing
print(error.localizedDescription)
}
}
activeTask = searchTask
}
}

View File

@ -131,29 +131,6 @@ final class SearchDetailViewController: UIViewController, NeedsDependency {
searchResultsOverviewViewController.view.pinToParent()
}
// bind search trigger
// "local" search
viewModel.searchText
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] searchText in
guard let self else { return }
self.searchResultsOverviewViewController.showStandardSearch(for: searchText)
}
.store(in: &disposeBag)
// delayed search on server
viewModel.searchText
.removeDuplicates()
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] searchText in
guard let self else { return }
self.searchResultsOverviewViewController.searchForSuggestions(for: searchText)
}
.store(in: &disposeBag)
// bind search history display
viewModel.searchText
.removeDuplicates()
@ -263,7 +240,11 @@ extension SearchDetailViewController {
extension SearchDetailViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
viewModel.searchText.value = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
viewModel.searchText.value = trimmedSearchText
searchResultsOverviewViewController.showStandardSearch(for: trimmedSearchText)
searchResultsOverviewViewController.searchForSuggestions(for: trimmedSearchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {

View File

@ -208,7 +208,6 @@ extension Mastodon.API {
return try Mastodon.API.decoder.decode(type, from: data)
} catch let decodeError {
#if DEBUG
os_log(.info, "%{public}s[%{public}ld], %{public}s: decode fail. content %s", ((#file as NSString).lastPathComponent), #line, #function, String(data: data, encoding: .utf8) ?? "<nil>")
debugPrint(decodeError)
#endif