[WIP] Remove CoreData for Tags/Accounts in Search (IOS-196)
- Add basic, json-based persistence (it's WIP and pragmatic aka dirty, see FileManager+SearchHistory)
This commit is contained in:
parent
aeaaf87b68
commit
59c6d31ca4
@ -161,6 +161,8 @@
|
||||
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
|
||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */; };
|
||||
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98782B0F622B0045EC2B /* SearchHistory.swift */; };
|
||||
D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; };
|
||||
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; };
|
||||
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; };
|
||||
@ -832,6 +834,8 @@
|
||||
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SearchHistory.swift"; sourceTree = "<group>"; };
|
||||
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||
D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = "<group>"; };
|
||||
D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
|
||||
@ -1889,6 +1893,23 @@
|
||||
path = Localization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8AC98742B0F615E0045EC2B /* Persistence */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98772B0F62230045EC2B /* Model */,
|
||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8AC98772B0F62230045EC2B /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8E5C347296DB896007E76A7 /* Edit History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2174,6 +2195,7 @@
|
||||
DB427DD425BAA00100D1B89D /* Mastodon */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98742B0F615E0045EC2B /* Persistence */,
|
||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||
2D76319C25C151DE00929FB9 /* Diffable */,
|
||||
@ -3842,6 +3864,7 @@
|
||||
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */,
|
||||
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
|
||||
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
|
||||
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */,
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
@ -3849,6 +3872,7 @@
|
||||
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
||||
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */,
|
||||
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */,
|
||||
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */,
|
||||
DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */,
|
||||
|
54
Mastodon/Persistence/FileManager+SearchHistory.swift
Normal file
54
Mastodon/Persistence/FileManager+SearchHistory.swift
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
|
||||
extension FileManager {
|
||||
func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] {
|
||||
guard let path = documentsDirectory()?.appending(path: Persistence.searchHistory.filename).appendingPathExtension("json"),
|
||||
let data = try? Data(contentsOf: path)
|
||||
else { return [] }
|
||||
|
||||
let jsonDecoder = JSONDecoder()
|
||||
jsonDecoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
do {
|
||||
let searchItems = try jsonDecoder.decode([Persistence.SearchHistory.Item].self, from: data)
|
||||
.filter { $0.userID == userID }
|
||||
.sorted { $0.updatedAt < $1.updatedAt }
|
||||
|
||||
return searchItems
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws {
|
||||
guard let path = documentsDirectory()?.appending(path: Persistence.searchHistory.filename).appendingPathExtension("json") else { return }
|
||||
|
||||
var searchItems = (try? searchItems(forUser: newSearchItem.userID)) ?? []
|
||||
|
||||
searchItems.append(newSearchItem)
|
||||
|
||||
let jsonEncoder = JSONEncoder()
|
||||
jsonEncoder.dateEncodingStrategy = .iso8601
|
||||
do {
|
||||
let data = try jsonEncoder.encode(searchItems)
|
||||
try data.write(to: path)
|
||||
} catch {
|
||||
debugPrint(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func removeSearchHistory() {
|
||||
guard let path = documentsDirectory()?.appending(path: Persistence.searchHistory.filename).appendingPathExtension("json") else { return }
|
||||
|
||||
try? removeItem(at: path)
|
||||
}
|
||||
}
|
||||
|
||||
extension FileManager {
|
||||
func documentsDirectory() -> URL? {
|
||||
return self.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
}
|
||||
}
|
15
Mastodon/Persistence/Model/SearchHistory.swift
Normal file
15
Mastodon/Persistence/Model/SearchHistory.swift
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension Persistence.SearchHistory {
|
||||
struct Item: Codable {
|
||||
let updatedAt: Date
|
||||
let userID: Mastodon.Entity.Account.ID
|
||||
|
||||
let account: Mastodon.Entity.Account?
|
||||
let hashtag: Mastodon.Entity.Tag?
|
||||
}
|
||||
}
|
@ -14,14 +14,9 @@ extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func coordinateToHashtagScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
tag: DataSourceItem.TagKind
|
||||
tag: Mastodon.Entity.Tag
|
||||
) async {
|
||||
switch tag {
|
||||
case .entity(let entity):
|
||||
await coordinateToHashtagScene(provider: provider, tag: entity)
|
||||
case .record(let record):
|
||||
await coordinateToHashtagScene(provider: provider, tag: record)
|
||||
}
|
||||
await coordinateToHashtagScene(provider: provider, tag: tag)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
@ -11,107 +11,42 @@ import MastodonCore
|
||||
import UIKit
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
|
||||
static func responseToCreateSearchHistory(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
item: DataSourceItem
|
||||
) async {
|
||||
switch item {
|
||||
|
||||
case .status, .account(_, _):
|
||||
break // not create search history for status
|
||||
case .user(let record):
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
|
||||
guard let user = record.object(in: managedObjectContext) else { return }
|
||||
_ = Persistence.SearchHistory.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.SearchHistory.PersistContext(
|
||||
entity: .user(user),
|
||||
me: me,
|
||||
now: Date()
|
||||
)
|
||||
case .account(account: let account, relationship: _):
|
||||
let now = Date()
|
||||
let userID = provider.authContext.mastodonAuthenticationBox.userID
|
||||
let searchEntry = Persistence.SearchHistory.Item(
|
||||
updatedAt: now,
|
||||
userID: userID,
|
||||
account: account,
|
||||
hashtag: nil
|
||||
)
|
||||
} // end try? await managedObjectContext.performChanges { … }
|
||||
case .hashtag(let tag):
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||
|
||||
switch tag {
|
||||
case .entity(let entity):
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
|
||||
|
||||
let now = Date()
|
||||
|
||||
let result = Persistence.Tag.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Tag.PersistContext(
|
||||
domain: authenticationBox.domain,
|
||||
entity: entity,
|
||||
me: me,
|
||||
networkDate: now
|
||||
)
|
||||
)
|
||||
|
||||
_ = Persistence.SearchHistory.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.SearchHistory.PersistContext(
|
||||
entity: .hashtag(result.tag),
|
||||
me: me,
|
||||
now: now
|
||||
)
|
||||
)
|
||||
} // end try? await managedObjectContext.performChanges { … }
|
||||
case .record(let record):
|
||||
try? await managedObjectContext.performChanges {
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
|
||||
guard let tag = record.object(in: managedObjectContext) else { return }
|
||||
|
||||
let now = Date()
|
||||
|
||||
_ = Persistence.SearchHistory.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.SearchHistory.PersistContext(
|
||||
entity: .hashtag(tag),
|
||||
me: me,
|
||||
now: now
|
||||
)
|
||||
)
|
||||
} // end try? await managedObjectContext.performChanges { … }
|
||||
} // end switch tag { … }
|
||||
case .notification:
|
||||
assertionFailure()
|
||||
} // end switch item { … }
|
||||
} // end func
|
||||
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToDeleteSearchHistory(
|
||||
provider: DataSourceProvider & AuthContextProvider
|
||||
) async throws {
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return }
|
||||
let request = SearchHistory.sortedFetchRequest
|
||||
request.predicate = SearchHistory.predicate(
|
||||
domain: authenticationBox.domain,
|
||||
userID: authenticationBox.userID
|
||||
)
|
||||
let searchHistories = managedObjectContext.safeFetch(request)
|
||||
|
||||
for searchHistory in searchHistories {
|
||||
managedObjectContext.delete(searchHistory)
|
||||
}
|
||||
} // end try await managedObjectContext.performChanges { … }
|
||||
} // end func
|
||||
try? FileManager.default.addSearchItem(searchEntry)
|
||||
case .hashtag(let tag):
|
||||
|
||||
let now = Date()
|
||||
let userID = provider.authContext.mastodonAuthenticationBox.userID
|
||||
let searchEntry = Persistence.SearchHistory.Item(
|
||||
updatedAt: now,
|
||||
userID: userID,
|
||||
account: nil,
|
||||
hashtag: tag
|
||||
)
|
||||
|
||||
try? FileManager.default.addSearchItem(searchEntry)
|
||||
case .status:
|
||||
break
|
||||
case .user(_):
|
||||
break
|
||||
case .notification:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,18 +14,11 @@ import class CoreDataStack.Notification
|
||||
enum DataSourceItem: Hashable {
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case hashtag(tag: TagKind)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case notification(record: ManagedObjectRecord<Notification>)
|
||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
}
|
||||
|
||||
extension DataSourceItem {
|
||||
enum TagKind: Hashable {
|
||||
case entity(Mastodon.Entity.Tag)
|
||||
case record(ManagedObjectRecord<Tag>)
|
||||
}
|
||||
}
|
||||
|
||||
extension DataSourceItem {
|
||||
struct Source {
|
||||
let collectionViewCell: UICollectionViewCell?
|
||||
|
@ -5,6 +5,10 @@ import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
protocol SearchResultOverviewCoordinatorDelegate: AnyObject {
|
||||
func newSearchHistoryItemAdded(_ coordinator: SearchResultOverviewCoordinator)
|
||||
}
|
||||
|
||||
class SearchResultOverviewCoordinator: Coordinator {
|
||||
|
||||
let overviewViewController: SearchResultsOverviewTableViewController
|
||||
@ -12,6 +16,8 @@ class SearchResultOverviewCoordinator: Coordinator {
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
|
||||
weak var delegate: SearchResultOverviewCoordinatorDelegate?
|
||||
|
||||
var activeTask: Task<Void, Never>?
|
||||
|
||||
init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) {
|
||||
@ -37,13 +43,13 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
|
||||
|
||||
func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag) {
|
||||
Task {
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: viewController,
|
||||
tag: tag
|
||||
)
|
||||
await DataSourceFacade.coordinateToHashtagScene(provider: viewController,
|
||||
tag: tag)
|
||||
|
||||
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
|
||||
item: .hashtag(tag: .entity(tag)))
|
||||
item: .hashtag(tag: tag))
|
||||
|
||||
delegate?.newSearchHistoryItemAdded(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,27 +117,14 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
|
||||
}
|
||||
|
||||
func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) {
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
|
||||
Task {
|
||||
let user = try await managedObjectContext.perform {
|
||||
return Persistence.MastodonUser.fetch(in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
entity: account,
|
||||
cache: nil,
|
||||
networkDate: Date()
|
||||
))
|
||||
}
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: viewController,
|
||||
account: account)
|
||||
|
||||
if let user {
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: viewController,
|
||||
user: user.asRecord)
|
||||
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
|
||||
item: .account(account: account, relationship: nil))
|
||||
|
||||
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
|
||||
item: .user(record: user.asRecord))
|
||||
}
|
||||
delegate?.newSearchHistoryItemAdded(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,13 +73,7 @@ final class SearchDetailViewController: UIViewController, NeedsDependency {
|
||||
return searchBar
|
||||
}()
|
||||
|
||||
private(set) lazy var searchHistoryViewController: SearchHistoryViewController = {
|
||||
let searchHistoryViewController = SearchHistoryViewController()
|
||||
searchHistoryViewController.context = context
|
||||
searchHistoryViewController.coordinator = coordinator
|
||||
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
|
||||
return searchHistoryViewController
|
||||
}()
|
||||
private var searchHistoryViewController: SearchHistoryViewController
|
||||
|
||||
private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = {
|
||||
return searchResultOverviewCoordinator.overviewViewController
|
||||
@ -92,8 +86,14 @@ final class SearchDetailViewController: UIViewController, NeedsDependency {
|
||||
self.coordinator = sceneCoordinator
|
||||
|
||||
self.searchResultOverviewCoordinator = SearchResultOverviewCoordinator(appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator)
|
||||
self.searchHistoryViewController = SearchHistoryViewController()
|
||||
searchHistoryViewController.context = appContext
|
||||
searchHistoryViewController.coordinator = sceneCoordinator
|
||||
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: appContext, authContext: authContext)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
searchResultOverviewCoordinator.delegate = searchHistoryViewController
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
@ -6,10 +6,9 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum SearchHistoryItem: Hashable {
|
||||
case hashtag(ManagedObjectRecord<Tag>)
|
||||
case user(ManagedObjectRecord<MastodonUser>)
|
||||
case hashtag(Mastodon.Entity.Tag)
|
||||
case account(Mastodon.Entity.Account)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import MastodonSDK
|
||||
|
||||
enum SearchHistorySection: Hashable {
|
||||
case main
|
||||
@ -28,47 +29,44 @@ extension SearchHistorySection {
|
||||
configuration: Configuration
|
||||
) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
|
||||
|
||||
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = item.object(in: context.managedObjectContext) else { return }
|
||||
cell.condensedUserView.configure(with: user)
|
||||
}
|
||||
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, Mastodon.Entity.Account> { cell, indexPath, account in
|
||||
|
||||
cell.condensedUserView.configure(with: account)
|
||||
}
|
||||
|
||||
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, ManagedObjectRecord<Tag>> { cell, indexPath, item in
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
|
||||
var contentConfiguration = cell.defaultContentConfiguration()
|
||||
contentConfiguration.image = UIImage(systemName: "magnifyingglass")
|
||||
contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
contentConfiguration.text = "#" + hashtag.name
|
||||
cell.contentConfiguration = contentConfiguration
|
||||
}
|
||||
|
||||
|
||||
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, Mastodon.Entity.Tag> { cell, indexPath, hashtag in
|
||||
|
||||
var contentConfiguration = cell.defaultContentConfiguration()
|
||||
contentConfiguration.image = UIImage(systemName: "magnifyingglass")
|
||||
contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
|
||||
contentConfiguration.text = "#" + hashtag.name
|
||||
cell.contentConfiguration = contentConfiguration
|
||||
|
||||
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
||||
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
|
||||
guard let state = cell?.configurationState else {
|
||||
return .secondarySystemGroupedBackground
|
||||
}
|
||||
|
||||
|
||||
if state.isHighlighted || state.isSelected {
|
||||
return SystemTheme.tableViewCellSelectionBackgroundColor
|
||||
}
|
||||
return .secondarySystemGroupedBackground
|
||||
}
|
||||
|
||||
cell.backgroundConfiguration = backgroundConfiguration
|
||||
}
|
||||
|
||||
|
||||
let dataSource = UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||
switch item {
|
||||
case .user(let record):
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: userCellRegister,
|
||||
for: indexPath, item: record)
|
||||
case .hashtag(let record):
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: hashtagCellRegister,
|
||||
for: indexPath, item: record)
|
||||
case .account(let account):
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: userCellRegister,
|
||||
for: indexPath, item: account)
|
||||
case .hashtag(let tag):
|
||||
return collectionView.dequeueConfiguredReusableCell(
|
||||
using: hashtagCellRegister,
|
||||
for: indexPath, item: tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,10 +21,10 @@ extension SearchHistoryViewController: DataSourceProvider {
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .user(let record):
|
||||
return .user(record: record)
|
||||
case .hashtag(let record):
|
||||
return .hashtag(tag: .record(record))
|
||||
case .account(let account):
|
||||
return .account(account: account, relationship: nil)
|
||||
case .hashtag(let tag):
|
||||
return .hashtag(tag: tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ final class SearchHistoryViewController: UIViewController, NeedsDependency {
|
||||
configuration.headerMode = .supplementary
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.keyboardDismissMode = .onDrag
|
||||
return collectionView
|
||||
}()
|
||||
}
|
||||
@ -56,32 +57,30 @@ extension SearchHistoryViewController: UICollectionViewDelegate {
|
||||
defer {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
|
||||
Task {
|
||||
let source = DataSourceItem.Source(indexPath: indexPath)
|
||||
guard let item = await item(from: source) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
await DataSourceFacade.responseToCreateSearchHistory(
|
||||
provider: self,
|
||||
item: item
|
||||
)
|
||||
|
||||
|
||||
switch item {
|
||||
case .user(let record):
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: record
|
||||
)
|
||||
case .hashtag(let record):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
tag: record
|
||||
)
|
||||
default:
|
||||
assertionFailure()
|
||||
break
|
||||
case .account(account: let account, relationship: _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
|
||||
case .hashtag(let tag):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
tag: tag
|
||||
)
|
||||
default:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,14 +98,15 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa
|
||||
_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView,
|
||||
clearButtonDidPressed button: UIButton
|
||||
) {
|
||||
Task {
|
||||
try await DataSourceFacade.responseToDeleteSearchHistory(
|
||||
provider: self
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
button.isEnabled = false
|
||||
}
|
||||
}
|
||||
FileManager.default.removeSearchHistory()
|
||||
viewModel.items = []
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - SearchResultOverviewCoordinatorDelegate
|
||||
extension SearchHistoryViewController: SearchResultOverviewCoordinatorDelegate {
|
||||
func newSearchHistoryItemAdded(_ coordinator: SearchResultOverviewCoordinator) {
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? []
|
||||
}
|
||||
}
|
||||
|
@ -26,42 +26,30 @@ extension SearchHistoryViewModel {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>()
|
||||
snapshot.appendSections([.main])
|
||||
diffableDataSource?.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
searchHistoryFetchedResultController.$records
|
||||
|
||||
$items
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] records in
|
||||
.sink { [weak self] items in
|
||||
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
Task {
|
||||
do {
|
||||
let managedObjectContext = self.context.managedObjectContext
|
||||
let items: [SearchHistoryItem] = try await managedObjectContext.perform {
|
||||
var items: [SearchHistoryItem] = []
|
||||
|
||||
for record in records {
|
||||
guard let searchHistory = record.object(in: managedObjectContext) else { continue }
|
||||
if let user = searchHistory.account {
|
||||
items.append(.user(.init(objectID: user.objectID)))
|
||||
} else if let hashtag = searchHistory.hashtag {
|
||||
items.append(.hashtag(.init(objectID: hashtag.objectID)))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
let mostRecentItems = Array(items.prefix(10))
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(mostRecentItems, toSection: .main)
|
||||
await diffableDataSource.apply(snapshot, animatingDifferences: true)
|
||||
} catch {
|
||||
// do nothing
|
||||
let searchItems: [SearchHistoryItem] = items.compactMap {
|
||||
if let account = $0.account {
|
||||
return .account(account)
|
||||
} else if let tag = $0.hashtag {
|
||||
return .hashtag(tag)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
|
||||
let mostRecentItems = Array(searchItems.prefix(10))
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(mostRecentItems, toSection: .main)
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ final class SearchHistoryViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
|
||||
@Published public var items: [Persistence.SearchHistory.Item]
|
||||
|
||||
// output
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>?
|
||||
@ -24,10 +24,7 @@ final class SearchHistoryViewModel {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext)
|
||||
|
||||
searchHistoryFetchedResultController.domain.value = authContext.mastodonAuthenticationBox.domain
|
||||
searchHistoryFetchedResultController.userID.value = authContext.mastodonAuthenticationBox.userID
|
||||
self.items = (try? FileManager.default.searchItems(forUser: authContext.mastodonAuthenticationBox.userID)) ?? []
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum SearchResultItem: Hashable {
|
||||
case user(ManagedObjectRecord<MastodonUser>)
|
||||
case account(Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
case status(ManagedObjectRecord<Status>)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case bottomLoader(attribute: BottomLoaderAttribute)
|
||||
|
@ -41,23 +41,21 @@ extension SearchResultSection {
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
case .account(let account, let relationship):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
|
||||
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell }
|
||||
|
||||
cell.userView.setButtonState(.loading)
|
||||
cell.configure(
|
||||
me: me,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: UserTableViewCell.ViewModel(user: user,
|
||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()),
|
||||
configuration: configuration
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
delegate: configuration.userTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
return cell
|
||||
|
||||
return cell
|
||||
case .status(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
|
@ -21,12 +21,12 @@ extension SearchResultViewController: DataSourceProvider {
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .user(let record):
|
||||
return .user(record: record)
|
||||
case .account(let account, let relationship):
|
||||
return .account(account: account, relationship: relationship)
|
||||
case .status(let record):
|
||||
return .status(record: record)
|
||||
case .hashtag(let entity):
|
||||
return .hashtag(tag: .entity(entity))
|
||||
case .hashtag(let tag):
|
||||
return .hashtag(tag: tag)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@ -52,9 +52,8 @@ extension SearchResultViewController {
|
||||
)
|
||||
|
||||
switch item {
|
||||
case .account(account: _, relationship: _):
|
||||
// do nothing
|
||||
break
|
||||
case .account(let account, relationship: _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .status(let status):
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
extension SearchResultViewModel {
|
||||
|
||||
@ -33,13 +34,19 @@ extension SearchResultViewModel {
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
statusFetchedResultsController.$records,
|
||||
userFetchedResultsController.$records,
|
||||
$accounts,
|
||||
$hashtags
|
||||
)
|
||||
.map { statusRecords, userRecords, hashtags in
|
||||
.map { statusRecords, accounts, hashtags in
|
||||
var items: [SearchResultItem] = []
|
||||
|
||||
let userItems = userRecords.map { SearchResultItem.user($0) }
|
||||
|
||||
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
|
||||
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}
|
||||
|
||||
return (account: account, relationship: relationship)
|
||||
}
|
||||
|
||||
let userItems = accountsWithRelationship.map { SearchResultItem.account($0.account, relationship: $0.relationship) }
|
||||
items.append(contentsOf: userItems)
|
||||
|
||||
let hashtagItems = hashtags.map { SearchResultItem.hashtag(tag: $0) }
|
||||
|
@ -113,20 +113,26 @@ extension SearchResultViewModel.State {
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.search(
|
||||
let searchResults = try await viewModel.context.apiService.search(
|
||||
query: query,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
).value
|
||||
|
||||
// discard result when request not the latest one
|
||||
guard id == self.latestLoadingToken else { return }
|
||||
// discard result when state is not Loading
|
||||
guard stateMachine.currentState is Loading else { return }
|
||||
|
||||
let userIDs = response.value.accounts.map { $0.id }
|
||||
let statusIDs = response.value.statuses.map { $0.id }
|
||||
let accounts = searchResults.accounts
|
||||
|
||||
let isNoMore = userIDs.isEmpty && statusIDs.isEmpty
|
||||
let relationships = try await viewModel.context.apiService.relationship(
|
||||
forAccounts: accounts,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
let statusIDs = searchResults.statuses.map { $0.id }
|
||||
|
||||
let isNoMore = accounts.isEmpty && statusIDs.isEmpty
|
||||
|
||||
if viewModel.searchScope == .all || isNoMore {
|
||||
await enter(state: NoMore.self)
|
||||
@ -136,20 +142,34 @@ extension SearchResultViewModel.State {
|
||||
|
||||
// reset data source when the search is refresh
|
||||
if offset == nil {
|
||||
viewModel.userFetchedResultsController.userIDs = []
|
||||
viewModel.relationships = []
|
||||
viewModel.accounts = []
|
||||
viewModel.statusFetchedResultsController.statusIDs = []
|
||||
viewModel.hashtags = []
|
||||
}
|
||||
|
||||
viewModel.userFetchedResultsController.append(userIDs: userIDs)
|
||||
// due to combine relationships must be set first
|
||||
|
||||
var existingRelationships = viewModel.relationships
|
||||
for hashtag in relationships where !existingRelationships.contains(hashtag) {
|
||||
existingRelationships.append(hashtag)
|
||||
}
|
||||
viewModel.relationships = existingRelationships
|
||||
|
||||
viewModel.statusFetchedResultsController.append(statusIDs: statusIDs)
|
||||
|
||||
var hashtags = viewModel.hashtags
|
||||
for hashtag in response.value.hashtags where !hashtags.contains(hashtag) {
|
||||
hashtags.append(hashtag)
|
||||
var existingHashtags = viewModel.hashtags
|
||||
for hashtag in searchResults.hashtags where !existingHashtags.contains(hashtag) {
|
||||
existingHashtags.append(hashtag)
|
||||
}
|
||||
viewModel.hashtags = hashtags
|
||||
|
||||
viewModel.hashtags = existingHashtags
|
||||
|
||||
var existingAccounts = viewModel.accounts
|
||||
for hashtag in searchResults.accounts where !existingAccounts.contains(hashtag) {
|
||||
existingAccounts.append(hashtag)
|
||||
}
|
||||
viewModel.accounts = existingAccounts
|
||||
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ final class SearchResultViewModel {
|
||||
let searchScope: SearchScope
|
||||
let searchText: String
|
||||
@Published var hashtags: [Mastodon.Entity.Tag] = []
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
@Published var accounts: [Mastodon.Entity.Account] = []
|
||||
var relationships: [Mastodon.Entity.Relationship] = []
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
@ -50,12 +51,9 @@ final class SearchResultViewModel {
|
||||
self.authContext = authContext
|
||||
self.searchScope = searchScope
|
||||
self.searchText = searchText
|
||||
self.accounts = []
|
||||
self.relationships = []
|
||||
|
||||
self.userFetchedResultsController = UserFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalPredicate: nil
|
||||
)
|
||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
|
@ -8,7 +8,16 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Persistence { }
|
||||
public enum Persistence {
|
||||
case searchHistory
|
||||
|
||||
public var filename: String {
|
||||
switch self {
|
||||
case .searchHistory:
|
||||
return "search_history"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Persistence {
|
||||
|
@ -124,45 +124,6 @@ public class CondensedUserView: UIView {
|
||||
avatarImageView.prepareForReuse()
|
||||
}
|
||||
|
||||
public func configure(with user: MastodonUser) {
|
||||
let displayNameMetaContent: MetaContent
|
||||
do {
|
||||
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
displayNameMetaContent = try MastodonMetaContent.convert(document: content)
|
||||
} catch {
|
||||
displayNameMetaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
}
|
||||
|
||||
displayNameLabel.configure(content: displayNameMetaContent)
|
||||
acctLabel.text = user.acct
|
||||
followersLabel.attributedText = NSAttributedString(
|
||||
format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]),
|
||||
args: NSAttributedString(string: Self.metricFormatter.string(from: Int(user.followersCount)) ?? user.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))])
|
||||
)
|
||||
|
||||
avatarImageView.setImage(url: user.avatarImageURL())
|
||||
|
||||
if let verifiedLink = user.verifiedLink?.value {
|
||||
verifiedLinkImageView.image = UIImage(systemName: "checkmark")
|
||||
verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
let verifiedLinkMetaContent: MetaContent
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:])
|
||||
verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
} catch {
|
||||
verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink)
|
||||
}
|
||||
|
||||
verifiedLinkLabel.configure(content: verifiedLinkMetaContent)
|
||||
} else {
|
||||
verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle")
|
||||
verifiedLinkImageView.tintColor = .secondaryLabel
|
||||
|
||||
verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink))
|
||||
}
|
||||
}
|
||||
|
||||
public func configure(with account: Mastodon.Entity.Account, showFollowers: Bool = true) {
|
||||
let displayNameMetaContent: MetaContent
|
||||
do {
|
||||
|
Loading…
x
Reference in New Issue
Block a user