feature: add search history
This commit is contained in:
parent
90803fc544
commit
d800e10bd7
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -138,12 +138,18 @@
|
|||
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
|
||||
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="PrivateNote" representedClassName="PrivateNote" syncable="YES">
|
||||
<entity name="PrivateNote" representedClassName=".PrivateNote" syncable="YES">
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
|
||||
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="SearchHistory" representedClassName=".SearchHistory" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||
<relationship name="hashTag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName=".Status" syncable="YES">
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -201,8 +207,9 @@
|
|||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
<element name="SearchHistory" positionX="72" positionY="162" width="128" height="89"/>
|
||||
</elements>
|
||||
</model>
|
||||
</model>
|
|
@ -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)]
|
||||
}
|
||||
}
|
|
@ -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 = "<group>"; };
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||
2D0B7A0A261D5A5600B44727 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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<SearchResultSection, SearchResultItem> {
|
||||
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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
// 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<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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<SearchResultSection, SearchResultItem>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue