Merge branch 'develop' into remove_status

This commit is contained in:
Marcus Kida 2023-11-28 09:50:56 +01:00 committed by GitHub
commit 03aeb1fa7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 355 additions and 752 deletions

View File

@ -161,6 +161,8 @@
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; }; D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; }; D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.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 */; }; D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; };
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; }; D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; };
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
@ -1889,6 +1893,23 @@
path = Localization; path = Localization;
sourceTree = "<group>"; 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 */ = { D8E5C347296DB896007E76A7 /* Edit History */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2174,6 +2195,7 @@
DB427DD425BAA00100D1B89D /* Mastodon */ = { DB427DD425BAA00100D1B89D /* Mastodon */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D8AC98742B0F615E0045EC2B /* Persistence */,
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
DB427DE325BAA00100D1B89D /* Info.plist */, DB427DE325BAA00100D1B89D /* Info.plist */,
2D76319C25C151DE00929FB9 /* Diffable */, 2D76319C25C151DE00929FB9 /* Diffable */,
@ -3842,6 +3864,7 @@
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */, 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */,
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */,
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */, DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
@ -3849,6 +3872,7 @@
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */, DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */, DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */,
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */, DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */,
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */, DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */,
DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */, DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */,

View File

@ -0,0 +1,75 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonCore
extension FileManager {
func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] {
return try searchItems().filter { $0.userID == userID }
}
func searchItems() throws -> [Persistence.SearchHistory.Item] {
guard let documentsDirectory else { return [] }
let searchHistoryPath = Persistence.searchHistory.filepath(baseURL: documentsDirectory)
guard let data = try? Data(contentsOf: searchHistoryPath) else { return [] }
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601
do {
let searchItems = try jsonDecoder.decode([Persistence.SearchHistory.Item].self, from: data)
.sorted { $0.updatedAt > $1.updatedAt }
return searchItems
} catch {
return []
}
}
func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws {
guard let documentsDirectory else { return }
var searchItems = (try? searchItems()) ?? []
if let index = searchItems.firstIndex(of: newSearchItem) {
searchItems.remove(at: index)
}
searchItems.append(newSearchItem)
storeJSON(searchItems, .searchHistory)
}
private func storeJSON(_ encodable: Encodable, _ persistence: Persistence) {
guard let documentsDirectory else { return }
let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .iso8601
do {
let data = try jsonEncoder.encode(encodable)
let searchHistoryPath = persistence.filepath(baseURL: documentsDirectory)
try data.write(to: searchHistoryPath)
} catch {
debugPrint(error.localizedDescription)
}
}
func removeSearchHistory(forUser userID: String) {
guard let documentsDirectory else { return }
var searchItems = (try? searchItems()) ?? []
let newSearchItems = searchItems.filter { $0.userID != userID }
storeJSON(newSearchItems, .searchHistory)
}
}
extension FileManager {
public var documentsDirectory: URL? {
return self.urls(for: .documentDirectory, in: .userDomainMask).first
}
}

View File

@ -0,0 +1,25 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonCore
import MastodonSDK
extension Persistence.SearchHistory {
struct Item: Codable, Hashable, Equatable {
let updatedAt: Date
let userID: Mastodon.Entity.Account.ID
let account: Mastodon.Entity.Account?
let hashtag: Mastodon.Entity.Tag?
func hash(into hasher: inout Hasher) {
hasher.combine(userID)
hasher.combine(account)
hasher.combine(hashtag)
}
public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool {
return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID
}
}
}

View File

@ -14,14 +14,9 @@ extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToHashtagScene( static func coordinateToHashtagScene(
provider: DataSourceProvider & AuthContextProvider, provider: DataSourceProvider & AuthContextProvider,
tag: DataSourceItem.TagKind tag: Mastodon.Entity.Tag
) async { ) async {
switch tag { await coordinateToHashtagScene(provider: provider, tag: tag)
case .entity(let entity):
await coordinateToHashtagScene(provider: provider, tag: entity)
case .record(let record):
await coordinateToHashtagScene(provider: provider, tag: record)
}
} }
@MainActor @MainActor

View File

@ -17,101 +17,36 @@ extension DataSourceFacade {
item: DataSourceItem item: DataSourceItem
) async { ) async {
switch item { switch item {
case .account(account: let account, relationship: _):
case .status, .account(_, _): let now = Date()
break // not create search history for status let userID = provider.authContext.mastodonAuthenticationBox.userID
case .user(let record): let searchEntry = Persistence.SearchHistory.Item(
let authenticationBox = provider.authContext.mastodonAuthenticationBox updatedAt: now,
let managedObjectContext = provider.context.backgroundManagedObjectContext userID: userID,
account: account,
try? await managedObjectContext.performChanges { hashtag: nil
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()
) )
)
} // end try? await managedObjectContext.performChanges { } try? FileManager.default.addSearchItem(searchEntry)
case .hashtag(let tag): 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 now = Date()
let userID = provider.authContext.mastodonAuthenticationBox.userID
let result = Persistence.Tag.createOrMerge( let searchEntry = Persistence.SearchHistory.Item(
in: managedObjectContext, updatedAt: now,
context: Persistence.Tag.PersistContext( userID: userID,
domain: authenticationBox.domain, account: nil,
entity: entity, hashtag: tag
me: me,
networkDate: now
)
) )
_ = Persistence.SearchHistory.createOrMerge( try? FileManager.default.addSearchItem(searchEntry)
in: managedObjectContext, case .status:
context: Persistence.SearchHistory.PersistContext( break
entity: .hashtag(result.tag), case .user(_):
me: me, break
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: case .notification:
assertionFailure() break
} // 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
} }

View File

@ -72,7 +72,7 @@ extension DataSourceFacade {
) async throws { ) async throws {
switch buttonState { switch buttonState {
case .follow: case .follow:
try await DataSourceFacade.responseToUserFollowAction( _ = try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency, dependency: dependency,
user: user user: user
) )
@ -80,14 +80,14 @@ extension DataSourceFacade {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id) dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id)
case .request: case .request:
try await DataSourceFacade.responseToUserFollowAction( _ = try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency, dependency: dependency,
user: user user: user
) )
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id) dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id)
case .unfollow: case .unfollow:
try await DataSourceFacade.responseToUserFollowAction( _ = try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency, dependency: dependency,
user: user user: user
) )
@ -102,7 +102,7 @@ extension DataSourceFacade {
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id) dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id)
case .pending: case .pending:
try await DataSourceFacade.responseToUserFollowAction( _ = try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency, dependency: dependency,
user: user user: user
) )

View File

@ -14,18 +14,11 @@ import class CoreDataStack.Notification
enum DataSourceItem: Hashable { enum DataSourceItem: Hashable {
case status(record: MastodonStatus) case status(record: MastodonStatus)
case user(record: ManagedObjectRecord<MastodonUser>) case user(record: ManagedObjectRecord<MastodonUser>)
case hashtag(tag: TagKind) case hashtag(tag: Mastodon.Entity.Tag)
case notification(record: MastodonNotification) case notification(record: MastodonNotification)
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) 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 { extension DataSourceItem {
struct Source { struct Source {
let collectionViewCell: UICollectionViewCell? let collectionViewCell: UICollectionViewCell?

View File

@ -5,6 +5,10 @@ import MastodonCore
import MastodonSDK import MastodonSDK
import MastodonLocalization import MastodonLocalization
protocol SearchResultOverviewCoordinatorDelegate: AnyObject {
func newSearchHistoryItemAdded(_ coordinator: SearchResultOverviewCoordinator)
}
class SearchResultOverviewCoordinator: Coordinator { class SearchResultOverviewCoordinator: Coordinator {
let overviewViewController: SearchResultsOverviewTableViewController let overviewViewController: SearchResultsOverviewTableViewController
@ -12,6 +16,8 @@ class SearchResultOverviewCoordinator: Coordinator {
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
weak var delegate: SearchResultOverviewCoordinatorDelegate?
var activeTask: Task<Void, Never>? var activeTask: Task<Void, Never>?
init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) { init(appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) {
@ -37,13 +43,13 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag) { func showPosts(_ viewController: SearchResultsOverviewTableViewController, tag: Mastodon.Entity.Tag) {
Task { Task {
await DataSourceFacade.coordinateToHashtagScene( await DataSourceFacade.coordinateToHashtagScene(provider: viewController,
provider: viewController, tag: tag)
tag: tag
)
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController, await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
item: .hashtag(tag: .entity(tag))) item: .hashtag(tag: tag))
delegate?.newSearchHistoryItemAdded(self)
} }
} }
@ -99,27 +105,14 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
} }
func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) { func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) {
let managedObjectContext = context.managedObjectContext
let domain = authContext.mastodonAuthenticationBox.domain
Task { 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()
))
}
if let user {
await DataSourceFacade.coordinateToProfileScene(provider: viewController, await DataSourceFacade.coordinateToProfileScene(provider: viewController,
user: user.asRecord) account: account)
await DataSourceFacade.responseToCreateSearchHistory(provider: viewController, await DataSourceFacade.responseToCreateSearchHistory(provider: viewController,
item: .user(record: user.asRecord)) item: .account(account: account, relationship: nil))
}
delegate?.newSearchHistoryItemAdded(self)
} }
} }

View File

@ -73,13 +73,7 @@ final class SearchDetailViewController: UIViewController, NeedsDependency {
return searchBar return searchBar
}() }()
private(set) lazy var searchHistoryViewController: SearchHistoryViewController = { private var searchHistoryViewController: SearchHistoryViewController
let searchHistoryViewController = SearchHistoryViewController()
searchHistoryViewController.context = context
searchHistoryViewController.coordinator = coordinator
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
return searchHistoryViewController
}()
private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = { private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = {
return searchResultOverviewCoordinator.overviewViewController return searchResultOverviewCoordinator.overviewViewController
@ -92,8 +86,14 @@ final class SearchDetailViewController: UIViewController, NeedsDependency {
self.coordinator = sceneCoordinator self.coordinator = sceneCoordinator
self.searchResultOverviewCoordinator = SearchResultOverviewCoordinator(appContext: appContext, authContext: authContext, sceneCoordinator: 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) super.init(nibName: nil, bundle: nil)
searchResultOverviewCoordinator.delegate = searchHistoryViewController
} }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

View File

@ -6,10 +6,9 @@
// //
import Foundation import Foundation
import CoreData import MastodonSDK
import CoreDataStack
enum SearchHistoryItem: Hashable { enum SearchHistoryItem: Hashable {
case hashtag(ManagedObjectRecord<Tag>) case hashtag(Mastodon.Entity.Tag)
case user(ManagedObjectRecord<MastodonUser>) case account(Mastodon.Entity.Account)
} }

View File

@ -9,6 +9,7 @@ import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore import MastodonCore
import MastodonAsset import MastodonAsset
import MastodonSDK
enum SearchHistorySection: Hashable { enum SearchHistorySection: Hashable {
case main case main
@ -28,22 +29,18 @@ extension SearchHistorySection {
configuration: Configuration configuration: Configuration
) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> { ) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, Mastodon.Entity.Account> { cell, indexPath, account in
context.managedObjectContext.performAndWait {
guard let user = item.object(in: context.managedObjectContext) else { return } cell.condensedUserView.configure(with: account)
cell.condensedUserView.configure(with: user)
}
} }
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, ManagedObjectRecord<Tag>> { cell, indexPath, item in let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, Mastodon.Entity.Tag> { cell, indexPath, hashtag in
context.managedObjectContext.performAndWait {
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
var contentConfiguration = cell.defaultContentConfiguration() var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.image = UIImage(systemName: "magnifyingglass") contentConfiguration.image = UIImage(systemName: "magnifyingglass")
contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
contentConfiguration.text = "#" + hashtag.name contentConfiguration.text = "#" + hashtag.name
cell.contentConfiguration = contentConfiguration cell.contentConfiguration = contentConfiguration
}
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in backgroundConfiguration.backgroundColorTransformer = .init { [weak cell] _ in
@ -56,19 +53,20 @@ extension SearchHistorySection {
} }
return .secondarySystemGroupedBackground return .secondarySystemGroupedBackground
} }
cell.backgroundConfiguration = backgroundConfiguration cell.backgroundConfiguration = backgroundConfiguration
} }
let dataSource = UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>(collectionView: collectionView) { collectionView, indexPath, item in let dataSource = UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>(collectionView: collectionView) { collectionView, indexPath, item in
switch item { switch item {
case .user(let record): case .account(let account):
return collectionView.dequeueConfiguredReusableCell( return collectionView.dequeueConfiguredReusableCell(
using: userCellRegister, using: userCellRegister,
for: indexPath, item: record) for: indexPath, item: account)
case .hashtag(let record): case .hashtag(let tag):
return collectionView.dequeueConfiguredReusableCell( return collectionView.dequeueConfiguredReusableCell(
using: hashtagCellRegister, using: hashtagCellRegister,
for: indexPath, item: record) for: indexPath, item: tag)
} }
} }

View File

@ -22,10 +22,10 @@ extension SearchHistoryViewController: DataSourceProvider {
} }
switch item { switch item {
case .user(let record): case .account(let account):
return .user(record: record) return .account(account: account, relationship: nil)
case .hashtag(let record): case .hashtag(let tag):
return .hashtag(tag: .record(record)) return .hashtag(tag: tag)
} }
} }

View File

@ -27,6 +27,7 @@ final class SearchHistoryViewController: UIViewController, NeedsDependency {
configuration.headerMode = .supplementary configuration.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: configuration) let layout = UICollectionViewCompositionalLayout.list(using: configuration)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.keyboardDismissMode = .onDrag
return collectionView return collectionView
}() }()
} }
@ -47,6 +48,11 @@ extension SearchHistoryViewController {
searchHistorySectionHeaderCollectionReusableViewDelegate: self searchHistorySectionHeaderCollectionReusableViewDelegate: self
) )
} }
override func viewWillAppear(_ animated: Bool) {
let userID = authContext.mastodonAuthenticationBox.userID
viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? []
}
} }
// MARK: - UICollectionViewDelegate // MARK: - UICollectionViewDelegate
@ -69,15 +75,13 @@ extension SearchHistoryViewController: UICollectionViewDelegate {
) )
switch item { switch item {
case .user(let record): case .account(account: let account, relationship: _):
await DataSourceFacade.coordinateToProfileScene( await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
provider: self,
user: record case .hashtag(let tag):
)
case .hashtag(let record):
await DataSourceFacade.coordinateToHashtagScene( await DataSourceFacade.coordinateToHashtagScene(
provider: self, provider: self,
tag: record tag: tag
) )
default: default:
assertionFailure() assertionFailure()
@ -99,14 +103,17 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa
_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView, _ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView,
clearButtonDidPressed button: UIButton clearButtonDidPressed button: UIButton
) { ) {
Task { let userID = authContext.mastodonAuthenticationBox.userID
try await DataSourceFacade.responseToDeleteSearchHistory(
provider: self
)
await MainActor.run { FileManager.default.removeSearchHistory(forUser: userID)
button.isEnabled = false viewModel.items = []
} }
} }
//MARK: - SearchResultOverviewCoordinatorDelegate
extension SearchHistoryViewController: SearchResultOverviewCoordinatorDelegate {
func newSearchHistoryItemAdded(_ coordinator: SearchResultOverviewCoordinator) {
let userID = authContext.mastodonAuthenticationBox.userID
viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? []
} }
} }

View File

@ -27,41 +27,29 @@ extension SearchHistoryViewModel {
snapshot.appendSections([.main]) snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot, animatingDifferences: false) diffableDataSource?.apply(snapshot, animatingDifferences: false)
searchHistoryFetchedResultController.$records $items
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] items in
guard let self = self else { return } guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return } guard let diffableDataSource = self.diffableDataSource else { return }
Task { let searchItems: [SearchHistoryItem] = items.compactMap {
do { if let account = $0.account {
let managedObjectContext = self.context.managedObjectContext return .account(account)
let items: [SearchHistoryItem] = try await managedObjectContext.perform { } else if let tag = $0.hashtag {
var items: [SearchHistoryItem] = [] return .hashtag(tag)
} else {
for record in records { return nil
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(searchItems.prefix(10))
}
let mostRecentItems = Array(items.prefix(10))
var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>() var snapshot = NSDiffableDataSourceSnapshot<SearchHistorySection, SearchHistoryItem>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems(mostRecentItems, toSection: .main) snapshot.appendItems(mostRecentItems, toSection: .main)
await diffableDataSource.apply(snapshot, animatingDifferences: true) diffableDataSource.apply(snapshot, animatingDifferences: true)
} catch {
// do nothing
}
} // end Task
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
} }

View File

@ -16,7 +16,7 @@ final class SearchHistoryViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
let searchHistoryFetchedResultController: SearchHistoryFetchedResultController @Published public var items: [Persistence.SearchHistory.Item]
// output // output
var diffableDataSource: UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>? var diffableDataSource: UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>?
@ -24,10 +24,7 @@ final class SearchHistoryViewModel {
init(context: AppContext, authContext: AuthContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext) self.items = (try? FileManager.default.searchItems(forUser: authContext.mastodonAuthenticationBox.userID)) ?? []
searchHistoryFetchedResultController.domain.value = authContext.mastodonAuthenticationBox.domain
searchHistoryFetchedResultController.userID.value = authContext.mastodonAuthenticationBox.userID
} }
} }

View File

@ -11,7 +11,7 @@ import CoreDataStack
import MastodonSDK import MastodonSDK
enum SearchResultItem: Hashable { enum SearchResultItem: Hashable {
case user(ManagedObjectRecord<MastodonUser>) case account(Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
case status(MastodonStatus) case status(MastodonStatus)
case hashtag(tag: Mastodon.Entity.Tag) case hashtag(tag: Mastodon.Entity.Tag)
case bottomLoader(attribute: BottomLoaderAttribute) case bottomLoader(attribute: BottomLoaderAttribute)

View File

@ -41,23 +41,19 @@ extension SearchResultSection {
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item { switch item {
case .user(let record): case .account(let account, let relationship):
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return } guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell }
configure(
context: context, cell.userView.setButtonState(.loading)
authContext: authContext, cell.configure(
me: me,
tableView: tableView, tableView: tableView,
cell: cell, account: account,
viewModel: UserTableViewCell.ViewModel(user: user, relationship: relationship,
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), delegate: configuration.userTableViewCellDelegate
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()),
configuration: configuration
) )
}
return cell
case .status(let status): case .status(let status):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
configure( configure(

View File

@ -22,12 +22,12 @@ extension SearchResultViewController: DataSourceProvider {
} }
switch item { switch item {
case .user(let record): case .account(let account, let relationship):
return .user(record: record) return .account(account: account, relationship: relationship)
case .status(let record): case .status(let record):
return .status(record: record) return .status(record: record)
case .hashtag(let entity): case .hashtag(let tag):
return .hashtag(tag: .entity(entity)) return .hashtag(tag: tag)
default: default:
return nil return nil
} }
@ -57,9 +57,8 @@ extension SearchResultViewController {
) )
switch item { switch item {
case .account(account: _, relationship: _): case .account(let account, relationship: _):
// do nothing await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
break
case .status(let status): case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene( await DataSourceFacade.coordinateToStatusThreadScene(
provider: self, provider: self,

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import Combine import Combine
import MastodonSDK
extension SearchResultViewModel { extension SearchResultViewModel {
@ -33,13 +34,19 @@ extension SearchResultViewModel {
Publishers.CombineLatest3( Publishers.CombineLatest3(
statusFetchedResultsController.$records, statusFetchedResultsController.$records,
userFetchedResultsController.$records, $accounts,
$hashtags $hashtags
) )
.map { statusRecords, userRecords, hashtags in .map { statusRecords, accounts, hashtags in
var items: [SearchResultItem] = [] 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) items.append(contentsOf: userItems)
let hashtagItems = hashtags.map { SearchResultItem.hashtag(tag: $0) } let hashtagItems = hashtags.map { SearchResultItem.hashtag(tag: $0) }

View File

@ -113,20 +113,31 @@ extension SearchResultViewModel.State {
Task { Task {
do { do {
let response = try await viewModel.context.apiService.search( let searchResults = try await viewModel.context.apiService.search(
query: query, query: query,
authenticationBox: viewModel.authContext.mastodonAuthenticationBox authenticationBox: viewModel.authContext.mastodonAuthenticationBox
) ).value
// discard result when request not the latest one // discard result when request not the latest one
guard id == self.latestLoadingToken else { return } guard id == self.latestLoadingToken else { return }
// discard result when state is not Loading // discard result when state is not Loading
guard stateMachine.currentState is Loading else { return } guard stateMachine.currentState is Loading else { return }
let userIDs = response.value.accounts.map { $0.id } //<<<<<<< remove_status
let statusIDs = response.value.statuses.map { MastodonStatus.fromEntity($0) } // let userIDs = response.value.accounts.map { $0.id }
// let statusIDs = response.value.statuses.map { MastodonStatus.fromEntity($0) }
//=======
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 }
//>>>>>>> develop
let isNoMore = accounts.isEmpty && statusIDs.isEmpty
if viewModel.searchScope == .all || isNoMore { if viewModel.searchScope == .all || isNoMore {
await enter(state: NoMore.self) await enter(state: NoMore.self)
@ -136,19 +147,36 @@ extension SearchResultViewModel.State {
// reset data source when the search is refresh // reset data source when the search is refresh
if offset == nil { if offset == nil {
viewModel.userFetchedResultsController.userIDs = []
await viewModel.statusFetchedResultsController.reset() await viewModel.statusFetchedResultsController.reset()
viewModel.relationships = []
viewModel.accounts = []
//viewModel.statusFetchedResultsController.statusIDs = []
viewModel.hashtags = [] viewModel.hashtags = []
} }
viewModel.userFetchedResultsController.append(userIDs: userIDs) viewModel.userFetchedResultsController.append(userIDs: userIDs)
await viewModel.statusFetchedResultsController.appendRecords(statusIDs) await viewModel.statusFetchedResultsController.appendRecords(statusIDs)
var hashtags = viewModel.hashtags
for hashtag in response.value.hashtags where !hashtags.contains(hashtag) { var existingRelationships = viewModel.relationships
hashtags.append(hashtag) for hashtag in relationships where !existingRelationships.contains(hashtag) {
existingRelationships.append(hashtag)
} }
viewModel.hashtags = hashtags viewModel.relationships = existingRelationships
viewModel.statusFetchedResultsController.append(statusIDs: statusIDs)
var existingHashtags = viewModel.hashtags
for hashtag in searchResults.hashtags where !existingHashtags.contains(hashtag) {
existingHashtags.append(hashtag)
}
viewModel.hashtags = existingHashtags
var existingAccounts = viewModel.accounts
for hashtag in searchResults.accounts where !existingAccounts.contains(hashtag) {
existingAccounts.append(hashtag)
}
viewModel.accounts = existingAccounts
} catch { } catch {
await enter(state: Fail.self) await enter(state: Fail.self)

View File

@ -22,7 +22,8 @@ final class SearchResultViewModel {
let searchScope: SearchScope let searchScope: SearchScope
let searchText: String let searchText: String
@Published var hashtags: [Mastodon.Entity.Tag] = [] @Published var hashtags: [Mastodon.Entity.Tag] = []
let userFetchedResultsController: UserFetchedResultsController @Published var accounts: [Mastodon.Entity.Account] = []
var relationships: [Mastodon.Entity.Relationship] = []
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -51,12 +52,9 @@ final class SearchResultViewModel {
self.authContext = authContext self.authContext = authContext
self.searchScope = searchScope self.searchScope = searchScope
self.searchText = searchText self.searchText = searchText
self.accounts = []
self.relationships = []
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
self.statusFetchedResultsController = StatusFetchedResultsController() self.statusFetchedResultsController = StatusFetchedResultsController()
} }
} }

View File

@ -125,7 +125,6 @@
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/> <relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/> <relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/> <relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
<relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/> <relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/>
<relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/> <relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/> <relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
@ -176,16 +175,6 @@
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/> <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"/> <relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
</entity> </entity>
<entity name="SearchHistory" representedClassName="CoreDataStack.SearchHistory" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String" defaultValueString=""/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
</entity>
<entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES"> <entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/> <attribute name="domain" attributeType="String"/>
@ -236,7 +225,6 @@
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/> <relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/> <relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/> <relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
</entity> </entity>
<entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES"> <entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
@ -271,6 +259,5 @@
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" attributeType="String"/> <attribute name="url" attributeType="String"/>
<relationship name="followedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followedTags" inverseEntity="MastodonUser"/> <relationship name="followedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followedTags" inverseEntity="MastodonUser"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
</entity> </entity>
</model> </model>

View File

@ -68,7 +68,6 @@ final public class MastodonUser: NSManagedObject {
// one-to-many relationship // one-to-many relationship
@NSManaged public private(set) var statuses: Set<Status> @NSManaged public private(set) var statuses: Set<Status>
@NSManaged public private(set) var notifications: Set<Notification> @NSManaged public private(set) var notifications: Set<Notification>
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
// many-to-many relationship // many-to-many relationship
@NSManaged public private(set) var favourite: Set<Status> @NSManaged public private(set) var favourite: Set<Status>
@ -216,28 +215,6 @@ extension MastodonUser {
} }
extension MastodonUser {
public func findSearchHistory(
domain: String,
userID: MastodonUser.ID
) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == domain
&& searchHistory.userID == userID
}
}
public func findSearchHistory(for user: MastodonUser) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == user.domain
&& searchHistory.userID == user.id
}
}
}
// MARK: - AutoGenerateProperty // MARK: - AutoGenerateProperty
extension MastodonUser: AutoGenerateProperty { extension MastodonUser: AutoGenerateProperty {
// sourcery:inline:MastodonUser.AutoGenerateProperty // sourcery:inline:MastodonUser.AutoGenerateProperty

View File

@ -1,158 +0,0 @@
//
// SearchHistory.swift
// CoreDataStack
//
// Created by sxiaojian on 2021/4/7.
//
import Foundation
import CoreData
public final class SearchHistory: NSManagedObject {
public typealias ID = UUID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var identifier: ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var domain: String
// sourcery: autoGenerateProperty
@NSManaged public private(set) var userID: MastodonUser.ID
// sourcery: autoGenerateProperty
@NSManaged public private(set) var createAt: Date
// sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date
// many-to-one relationship
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var account: MastodonUser?
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var hashtag: Tag?
// sourcery: autoGenerateRelationship
@NSManaged public private(set) var status: Status?
}
extension SearchHistory {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property,
relationship: Relationship
) -> SearchHistory {
let object: SearchHistory = context.insertObject()
object.configure(property: property)
object.configure(relationship: relationship)
return object
}
}
extension SearchHistory: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)]
}
}
extension SearchHistory {
static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.domain), domain)
}
static func predicate(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.userID), userID)
}
public static func predicate(domain: String, userID: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate(domain: domain),
predicate(userID: userID)
])
}
}
// MARK: - AutoGenerateProperty
extension SearchHistory: AutoGenerateProperty {
// sourcery:inline:SearchHistory.AutoGenerateProperty
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
public let identifier: ID
public let domain: String
public let userID: MastodonUser.ID
public let createAt: Date
public let updatedAt: Date
public init(
identifier: ID,
domain: String,
userID: MastodonUser.ID,
createAt: Date,
updatedAt: Date
) {
self.identifier = identifier
self.domain = domain
self.userID = userID
self.createAt = createAt
self.updatedAt = updatedAt
}
}
public func configure(property: Property) {
self.identifier = property.identifier
self.domain = property.domain
self.userID = property.userID
self.createAt = property.createAt
self.updatedAt = property.updatedAt
}
public func update(property: Property) {
update(updatedAt: property.updatedAt)
}
// sourcery:end
}
// MARK: - AutoGenerateRelationship
extension SearchHistory: AutoGenerateRelationship {
// sourcery:inline:SearchHistory.AutoGenerateRelationship
// Generated using Sourcery
// DO NOT EDIT
public struct Relationship {
public let account: MastodonUser?
public let hashtag: Tag?
public let status: Status?
public init(
account: MastodonUser?,
hashtag: Tag?,
status: Status?
) {
self.account = account
self.hashtag = hashtag
self.status = status
}
}
public func configure(relationship: Relationship) {
self.account = relationship.account
self.hashtag = relationship.hashtag
self.status = relationship.status
}
// sourcery:end
}
// MARK: - AutoUpdatableObject
extension SearchHistory: AutoUpdatableObject {
// sourcery:inline:SearchHistory.AutoUpdatableObject
// Generated using Sourcery
// DO NOT EDIT
public func update(updatedAt: Date) {
if self.updatedAt != updatedAt {
self.updatedAt = updatedAt
}
}
// sourcery:end
}

View File

@ -98,7 +98,6 @@ public final class Status: NSManagedObject {
@NSManaged public private(set) var reblogFrom: Set<Status> @NSManaged public private(set) var reblogFrom: Set<Status>
@NSManaged public private(set) var replyFrom: Set<Status> @NSManaged public private(set) var replyFrom: Set<Status>
@NSManaged public private(set) var notifications: Set<Notification> @NSManaged public private(set) var notifications: Set<Notification>
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
// sourcery: autoUpdatableObject, autoGenerateProperty // sourcery: autoUpdatableObject, autoGenerateProperty
@NSManaged public private(set) var updatedAt: Date @NSManaged public private(set) var updatedAt: Date

View File

@ -31,9 +31,6 @@ public final class Tag: NSManagedObject {
// many-to-many relationship // many-to-many relationship
@NSManaged public private(set) var followedBy: Set<MastodonUser> @NSManaged public private(set) var followedBy: Set<MastodonUser>
// one-to-many relationship
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
} }
extension Tag { extension Tag {
@ -216,45 +213,3 @@ extension Tag: AutoUpdatableObject {
} }
} }
extension Tag {
public func findSearchHistory(domain: String, userID: MastodonUser.ID) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == domain
&& searchHistory.userID == userID
}
}
public func findSearchHistory(for user: MastodonUser) -> SearchHistory? {
return searchHistories.first { searchHistory in
return searchHistory.domain == user.domain
&& searchHistory.userID == user.id
}
}
}
public extension Tag {
// func updateHistory(index: Int, day: Date, uses: String, account: String) {
// let histories = self.histories.sorted {
// $0.createAt.compare($1.createAt) == .orderedAscending
// }
// guard index < histories.count else { return }
// let history = histories[index]
// history.update(day: day)
// history.update(uses: uses)
// history.update(accounts: account)
// }
//
// func appendHistory(history: History) {
// self.mutableSetValue(forKeyPath: #keyPath(Tag.histories)).add(history)
// }
//
// func update(url: String) {
// if self.url != url {
// self.url = url
// }
// }
}

View File

@ -1,77 +0,0 @@
//
// SearchHistoryFetchedResultController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-15.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
public final class SearchHistoryFetchedResultController: NSObject {
var disposeBag = Set<AnyCancellable>()
public let fetchedResultsController: NSFetchedResultsController<SearchHistory>
public let domain = CurrentValueSubject<String?, Never>(nil)
public let userID = CurrentValueSubject<Mastodon.Entity.Status.ID?, Never>(nil)
// output
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@Published public private(set) var records: [ManagedObjectRecord<SearchHistory>] = []
public init(managedObjectContext: NSManagedObjectContext) {
self.fetchedResultsController = {
let fetchRequest = SearchHistory.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init()
// debounce output to prevent UI update issues
_objectIDs
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
.assign(to: &$records)
fetchedResultsController.delegate = self
Publishers.CombineLatest(
self.domain,
self.userID
)
.receive(on: DispatchQueue.main)
.sink { [weak self] domain, userID in
guard let self = self else { return }
let predicates = [SearchHistory.predicate(domain: domain ?? "", userID: userID ?? "")]
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
do {
try self.fetchedResultsController.performFetch()
} catch {
assertionFailure(error.localizedDescription)
}
}
.store(in: &disposeBag)
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension SearchHistoryFetchedResultController: NSFetchedResultsControllerDelegate {
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
let objects = fetchedResultsController.fetchedObjects ?? []
self._objectIDs.value = objects.map { $0.objectID }
}
}

View File

@ -1,113 +0,0 @@
//
// Persistence+SearchHistory.swift
// Mastodon
//
// Created by MainasuK on 2022-1-20.
//
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
extension Persistence.SearchHistory {
public struct PersistContext {
public let entity: Entity
public let me: MastodonUser
public let now: Date
public init(
entity: Entity,
me: MastodonUser,
now: Date
) {
self.entity = entity
self.me = me
self.now = now
}
public enum Entity: Hashable {
case user(MastodonUser)
case hashtag(Tag)
}
}
public struct PersistResult {
public let searchHistory: SearchHistory
public let isNewInsertion: Bool
public init(
searchHistory: SearchHistory,
isNewInsertion: Bool
) {
self.searchHistory = searchHistory
self.isNewInsertion = isNewInsertion
}
}
public static func createOrMerge(
in managedObjectContext: NSManagedObjectContext,
context: PersistContext
) -> PersistResult {
if let old = fetch(in: managedObjectContext, context: context) {
update(searchHistory: old, context: context)
return PersistResult(searchHistory: old, isNewInsertion: false)
} else {
let object = create(in: managedObjectContext, context: context)
return PersistResult(searchHistory: object, isNewInsertion: true)
}
}
}
extension Persistence.SearchHistory {
public static func fetch(
in managedObjectContext: NSManagedObjectContext,
context: PersistContext
) -> SearchHistory? {
switch context.entity {
case .user(let user):
return user.findSearchHistory(for: context.me)
case .hashtag(let hashtag):
return hashtag.findSearchHistory(for: context.me)
}
}
@discardableResult
public static func create(
in managedObjectContext: NSManagedObjectContext,
context: PersistContext
) -> SearchHistory {
let property = SearchHistory.Property(
identifier: UUID(),
domain: context.me.domain,
userID: context.me.id,
createAt: context.now,
updatedAt: context.now
)
let relationship: SearchHistory.Relationship = {
switch context.entity {
case .user(let user):
return SearchHistory.Relationship(account: user, hashtag: nil, status: nil)
case .hashtag(let hashtag):
return SearchHistory.Relationship(account: nil, hashtag: hashtag, status: nil)
}
}()
let searchHistory = SearchHistory.insert(
into: managedObjectContext,
property: property,
relationship: relationship
)
update(searchHistory: searchHistory, context: context)
return searchHistory
}
private static func update(
searchHistory: SearchHistory,
context: PersistContext
) {
searchHistory.update(updatedAt: context.now)
}
}

View File

@ -8,7 +8,22 @@
import Foundation import Foundation
public enum Persistence { } public enum Persistence {
case searchHistory
private var filename: String {
switch self {
case .searchHistory:
return "search_history"
}
}
public func filepath(baseURL: URL) -> URL {
baseURL
.appending(path: filename)
.appendingPathExtension("json")
}
}
extension Persistence { extension Persistence {

View File

@ -124,45 +124,6 @@ public class CondensedUserView: UIView {
avatarImageView.prepareForReuse() 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) { public func configure(with account: Mastodon.Entity.Account, showFollowers: Bool = true) {
let displayNameMetaContent: MetaContent let displayNameMetaContent: MetaContent
do { do {