From d800e10bd7b069f61b812730695618a357330f7d Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 7 Apr 2021 19:49:33 +0800 Subject: [PATCH] feature: add search history --- .../CoreData.xcdatamodel/contents | 17 +- CoreDataStack/Entity/SearchHistory.swift | 54 +++++++ Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Item/SearchResultItem.swift | 13 ++ .../Section/SearchResultSection.swift | 16 +- .../SearchViewController+Searching.swift | 48 +++++- .../Scene/Search/SearchViewController.swift | 29 +++- Mastodon/Scene/Search/SearchViewModel.swift | 145 +++++++++++++++--- .../SearchingTableViewCell.swift | 31 +++- .../SearchRecommendCollectionHeader.swift | 2 +- 10 files changed, 325 insertions(+), 34 deletions(-) create mode 100644 CoreDataStack/Entity/SearchHistory.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index d655753d3..5f048880f 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -138,12 +138,18 @@ - + + + + + + + @@ -201,8 +207,9 @@ - - + + + - + \ No newline at end of file diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift new file mode 100644 index 000000000..c1a81bc05 --- /dev/null +++ b/CoreDataStack/Entity/SearchHistory.swift @@ -0,0 +1,54 @@ +// +// SearchHistory.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/4/7. +// + +import Foundation +import CoreData + +public final class SearchHistory: NSManagedObject { + public typealias ID = UUID + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var createAt: Date + + @NSManaged public private(set) var account: MastodonUser? + @NSManaged public private(set) var hashTag: Tag? + +} + +extension SearchHistory { + public override func awakeFromInsert() { + super.awakeFromInsert() + setPrimitiveValue(UUID(), forKey: #keyPath(SearchHistory.identifier)) + } + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + account: MastodonUser + ) -> SearchHistory { + let searchHistory: SearchHistory = context.insertObject() + searchHistory.account = account + searchHistory.createAt = Date() + return searchHistory + } + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + hashTag: Tag + ) -> SearchHistory { + let searchHistory: SearchHistory = context.insertObject() + searchHistory.hashTag = hashTag + searchHistory.createAt = Date() + return searchHistory + } +} + +extension SearchHistory: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \SearchHistory.createAt, ascending: false)] + } +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 165b9af31..214aece57 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; 2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A0A261D5A5600B44727 /* Array.swift */; }; + 2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; }; 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; }; 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; @@ -372,6 +373,7 @@ 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; 2D0B7A0A261D5A5600B44727 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; + 2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; @@ -1398,6 +1400,7 @@ isa = PBXGroup; children = ( DB89BA2625C110B4008580ED /* Status.swift */, + 2D0B7A1C261D839600B44727 /* SearchHistory.swift */, DB8AF52425C131D1002E6C99 /* MastodonUser.swift */, DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */, 2D927F0125C7E4F2004F19B8 /* Mention.swift */, @@ -2333,6 +2336,7 @@ 2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */, 2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */, DB89BA1D25C1107F008580ED /* URL.swift in Sources */, + 2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */, 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift index 1156a05fa..56390f203 100644 --- a/Mastodon/Diffiable/Item/SearchResultItem.swift +++ b/Mastodon/Diffiable/Item/SearchResultItem.swift @@ -5,6 +5,7 @@ // Created by sxiaojian on 2021/4/6. // +import CoreData import Foundation import MastodonSDK @@ -13,6 +14,10 @@ enum SearchResultItem { case account(account: Mastodon.Entity.Account) + case accountObjectID(accountObjectID: NSManagedObjectID) + + case hashTagObjectID(hashTagObjectID: NSManagedObjectID) + case bottomLoader } @@ -25,6 +30,10 @@ extension SearchResultItem: Equatable { return accountLeft == accountRight case (.bottomLoader, .bottomLoader): return true + case (.accountObjectID(let idLeft),.accountObjectID(let idRight)): + return idLeft == idRight + case (.hashTagObjectID(let idLeft),.hashTagObjectID(let idRight)): + return idLeft == idRight default: return false } @@ -38,6 +47,10 @@ extension SearchResultItem: Hashable { hasher.combine(account) case .hashTag(let tag): hasher.combine(tag) + case .accountObjectID(let id): + hasher.combine(id) + case .hashTagObjectID(let id): + hasher.combine(id) case .bottomLoader: hasher.combine(String(describing: SearchResultItem.bottomLoader.self)) } diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift index 91e443bdc..66f6891e4 100644 --- a/Mastodon/Diffiable/Section/SearchResultSection.swift +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -8,16 +8,20 @@ import Foundation import MastodonSDK import UIKit +import CoreData +import CoreDataStack enum SearchResultSection: Equatable, Hashable { case account case hashTag + case mixed case bottomLoader } extension SearchResultSection { static func tableViewDiffableDataSource( - for tableView: UITableView + for tableView: UITableView, + dependency: NeedsDependency ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in switch result { @@ -29,6 +33,16 @@ extension SearchResultSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell cell.config(with: tag) return cell + case .hashTagObjectID(let hashTagObjectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell + let tag = dependency.context.managedObjectContext.object(with: hashTagObjectID) as! Tag + cell.config(with: tag) + return cell + case .accountObjectID(let accountObjectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell + let user = dependency.context.managedObjectContext.object(with: accountObjectID) as! MastodonUser + cell.config(with: user) + return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader cell.startAnimating() diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 51281f30c..34acf443f 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -5,8 +5,12 @@ // Created by sxiaojian on 2021/4/2. // +import Combine +import CoreData +import CoreDataStack import Foundation import MastodonSDK +import OSLog import UIKit extension SearchViewController { @@ -20,7 +24,7 @@ extension SearchViewController { searchingTableView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), searchingTableView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), searchingTableView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor), + searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor) ]) searchingTableView.tableFooterView = UIView() viewModel.isSearching @@ -29,6 +33,42 @@ extension SearchViewController { self?.searchingTableView.isHidden = !isSearching } .store(in: &disposeBag) + + Publishers.CombineLatest( + viewModel.isSearching, + viewModel.searchText + ) + .sink { [weak self] isSearching, text in + guard let self = self else { return } + if isSearching, text.isEmpty { + self.searchingTableView.tableHeaderView = self.searchHeader + } else { + self.searchingTableView.tableHeaderView = nil + } + } + .store(in: &disposeBag) + } + + func setupSearchHeader() { + searchHeader.addSubview(recentSearchesLabel) + recentSearchesLabel.constrain([ + recentSearchesLabel.constraint(.leading, toView: searchHeader, constant: 16), + recentSearchesLabel.constraint(.centerY, toView: searchHeader) + ]) + + searchHeader.addSubview(clearSearchHistoryButton) + recentSearchesLabel.constrain([ + searchHeader.trailingAnchor.constraint(equalTo: clearSearchHistoryButton.trailingAnchor, constant: 16), + clearSearchHistoryButton.constraint(.centerY, toView: searchHeader) + ]) + + clearSearchHistoryButton.addTarget(self, action: #selector(SearchViewController.clearAction(_:)), for: .touchUpInside) + } +} + +extension SearchViewController { + @objc func clearAction(_ sender: UIButton) { + viewModel.deleteSearchHistory() } } @@ -43,5 +83,9 @@ extension SearchViewController: UITableViewDelegate { 66 } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + viewModel.saveItemToCoreData(item: item) + } } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 0b26cdb40..1dfa87e77 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -76,17 +76,39 @@ final class SearchViewController: UIViewController, NeedsDependency { // searching let searchingTableView: UITableView = { let tableView = UITableView() + tableView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .singleLine - tableView.backgroundColor = .white return tableView }() + + lazy var searchHeader: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + view.frame = CGRect(origin: .zero, size: CGSize(width: searchingTableView.frame.width, height: 56)) + return view + }() + + let recentSearchesLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + label.textColor = Asset.Colors.Label.primary.color + label.text = L10n.Scene.Search.Searching.recentSearch + return label + }() + + let clearSearchHistoryButton: UIButton = { + let button = UIButton(type: .custom) + button.setTitleColor(Asset.Colors.buttonDefault.color, for: .normal) + button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal) + return button + }() } extension SearchViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = Asset.Colors.Background.search.color + view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color searchBar.delegate = self navigationItem.titleView = searchBar navigationItem.hidesBackButton = true @@ -95,6 +117,7 @@ extension SearchViewController { setupAccountsCollectionView() setupSearchingTableView() setupDataSource() + setupSearchHeader() } func setupScrollView() { @@ -120,7 +143,7 @@ extension SearchViewController { func setupDataSource() { viewModel.hashTagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashTagCollectionView) viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView) - viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView) + viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 4fbdab5ba..6827c7035 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -6,13 +6,15 @@ // import Combine +import CoreData +import CoreDataStack import Foundation import GameplayKit import MastodonSDK import OSLog import UIKit -final class SearchViewModel { +final class SearchViewModel: NSObject { var disposeBag = Set() // input @@ -51,41 +53,79 @@ final class SearchViewModel { init(context: AppContext) { self.context = context + super.init() + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } Publishers.CombineLatest( searchText - .filter { !$0.isEmpty } .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), - searchScope) - .flatMap { (text, scope) -> AnyPublisher, Error> in - let query = Mastodon.API.Search.Query(accountID: nil, - maxID: nil, - minID: nil, - type: scope, - excludeUnreviewed: nil, - q: text, - resolve: nil, - limit: nil, - offset: nil, - following: nil) - return context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox) - } - .sink { _ in - } receiveValue: { [weak self] result in - self?.searchResult.value = result.value - } - .store(in: &disposeBag) + searchScope + ) + .filter { text, _ in + !text.isEmpty + } + .flatMap { (text, scope) -> AnyPublisher, Error> in + + let query = Mastodon.API.Search.Query(accountID: nil, + maxID: nil, + minID: nil, + type: scope, + excludeUnreviewed: nil, + q: text, + resolve: nil, + limit: nil, + offset: nil, + following: nil) + return context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + } + .sink { _ in + } receiveValue: { [weak self] result in + self?.searchResult.value = result.value + } + .store(in: &disposeBag) isSearching .sink { [weak self] isSearching in if !isSearching { self?.searchResult.value = nil + self?.searchText.value = "" } } .store(in: &disposeBag) + Publishers.CombineLatest3( + isSearching, + searchText, + searchScope + ) + .filter { isSearching, text, _ in + isSearching && text.isEmpty + } + .sink { [weak self] _, _, scope in + guard let self = self else { return } + guard let searchHistories = self.fetchSearchHistory() else { return } + guard let dataSource = self.searchResultDiffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.mixed]) + + searchHistories.forEach { searchHistory in + let containsAccount = scope == Mastodon.API.Search.Scope.accounts.rawValue || scope == "" + let containsHashTag = scope == Mastodon.API.Search.Scope.hashTags.rawValue || scope == "" + if let mastodonUser = searchHistory.account, containsAccount { + let item = SearchResultItem.accountObjectID(accountObjectID: mastodonUser.objectID) + snapshot.appendItems([item], toSection: .mixed) + } + if let tag = searchHistory.hashTag, containsHashTag { + let item = SearchResultItem.hashTagObjectID(hashTagObjectID: tag.objectID) + snapshot.appendItems([item], toSection: .mixed) + } + } + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + .store(in: &disposeBag) + requestRecommendHashTags() .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -190,4 +230,67 @@ final class SearchViewModel { .store(in: &self.disposeBag) } } + + func saveItemToCoreData(item: SearchResultItem) { + _ = context.managedObjectContext.performChanges { [weak self] in + guard let self = self else { return } + switch item { + case .account(let account): + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + // load request mastodon user + let requestMastodonUser: MastodonUser? = { + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, id: activeMastodonAuthenticationBox.userID) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try self.context.managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.context.managedObjectContext, for: requestMastodonUser, in: activeMastodonAuthenticationBox.domain, entity: account, userCache: nil, networkDate: Date(), log: OSLog.api) + SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser) + + case .hashTag(let tag): + let histories = tag.history?[0 ... 2].compactMap { history -> History in + History.insert(into: self.context.managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) + } + let tagInCoreData = Tag.insert(into: self.context.managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) + SearchHistory.insert(into: self.context.managedObjectContext, hashTag: tagInCoreData) + + default: + break + } + } + } + + func fetchSearchHistory() -> [SearchHistory]? { + let searchHistory: [SearchHistory]? = { + let request = SearchHistory.sortedFetchRequest + request.predicate = nil + request.returnsObjectsAsFaults = false + do { + return try context.managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return nil + } + + }() + return searchHistory + } + + func deleteSearchHistory() { + let result = fetchSearchHistory() + _ = context.managedObjectContext.performChanges { [weak self] in + result?.forEach { history in + self?.context.managedObjectContext.delete(history) + } + self?.isSearching.value = true + } + } } diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index 379d720ea..cdfcdce23 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -5,6 +5,8 @@ // Created by sxiaojian on 2021/4/2. // +import CoreData +import CoreDataStack import Foundation import MastodonSDK import UIKit @@ -12,7 +14,7 @@ import UIKit final class SearchingTableViewCell: UITableViewCell { let _imageView: UIImageView = { let imageView = UIImageView() - imageView.tintColor = .black + imageView.tintColor = Asset.Colors.Label.primary.color return imageView }() @@ -50,6 +52,7 @@ final class SearchingTableViewCell: UITableViewCell { extension SearchingTableViewCell { private func configure() { + backgroundColor = .clear selectionStyle = .none contentView.addSubview(_imageView) _imageView.pin(toSize: CGSize(width: 42, height: 42)) @@ -75,6 +78,16 @@ extension SearchingTableViewCell { _subTitleLabel.text = account.acct } + func config(with account: MastodonUser) { + _imageView.af.setImage( + withURL: URL(string: account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + _titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName + _subTitleLabel.text = account.acct + } + func config(with tag: Mastodon.Entity.Tag) { let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) _imageView.image = image @@ -88,6 +101,22 @@ extension SearchingTableViewCell { let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) _subTitleLabel.text = string } + + func config(with tag: Tag) { + let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) + _imageView.image = image + _titleLabel.text = "# " + tag.name + guard let historys = tag.histories?.sorted(by: { + $0.createAt.compare($1.createAt) == .orderedAscending + }) else { + _subTitleLabel.text = "" + return + } + let recentHistory = historys[0 ... 2] + let peopleAreTalking = recentHistory.compactMap { Int($0.accounts) }.reduce(0, +) + let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) + _subTitleLabel.text = string + } } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index 193da2c47..ebd60ac30 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -18,7 +18,7 @@ class SearchRecommendCollectionHeader: UIView { let descriptionLabel: UILabel = { let label = UILabel() - label.textColor = Asset.Colors.lightSecondaryText.color + label.textColor = Asset.Colors.Label.secondary.color label.font = .preferredFont(forTextStyle: .body) label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping