Merge branch 'develop' into remove_status
This commit is contained in:
commit
03aeb1fa7e
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ extension DataSourceFacade {
|
|||
) async throws {
|
||||
switch buttonState {
|
||||
case .follow:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
@ -80,21 +80,21 @@ extension DataSourceFacade {
|
|||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id)
|
||||
|
||||
case .request:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id)
|
||||
case .unfollow:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == user.id })
|
||||
case .blocked:
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
@ -102,7 +102,7 @@ extension DataSourceFacade {
|
|||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id)
|
||||
|
||||
case .pending:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
|
|
@ -14,18 +14,11 @@ import class CoreDataStack.Notification
|
|||
enum DataSourceItem: Hashable {
|
||||
case status(record: MastodonStatus)
|
||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case hashtag(tag: TagKind)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case notification(record: MastodonNotification)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,27 +105,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,10 +22,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
|
||||
}()
|
||||
}
|
||||
|
@ -47,6 +48,11 @@ extension SearchHistoryViewController {
|
|||
searchHistorySectionHeaderCollectionReusableViewDelegate: self
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate
|
||||
|
@ -56,32 +62,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 +103,17 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa
|
|||
_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView,
|
||||
clearButtonDidPressed button: UIButton
|
||||
) {
|
||||
Task {
|
||||
try await DataSourceFacade.responseToDeleteSearchHistory(
|
||||
provider: self
|
||||
)
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
|
||||
await MainActor.run {
|
||||
button.isEnabled = false
|
||||
}
|
||||
}
|
||||
FileManager.default.removeSearchHistory(forUser: userID)
|
||||
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(MastodonStatus)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case bottomLoader(attribute: BottomLoaderAttribute)
|
||||
|
|
|
@ -41,23 +41,19 @@ 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
|
||||
case .status(let status):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
configure(
|
||||
|
|
|
@ -22,12 +22,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
|
||||
}
|
||||
|
@ -57,9 +57,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,31 @@ 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 { MastodonStatus.fromEntity($0) }
|
||||
//<<<<<<< remove_status
|
||||
// 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 {
|
||||
await enter(state: NoMore.self)
|
||||
|
@ -136,20 +147,37 @@ extension SearchResultViewModel.State {
|
|||
|
||||
// reset data source when the search is refresh
|
||||
if offset == nil {
|
||||
viewModel.userFetchedResultsController.userIDs = []
|
||||
await viewModel.statusFetchedResultsController.reset()
|
||||
viewModel.relationships = []
|
||||
viewModel.accounts = []
|
||||
//viewModel.statusFetchedResultsController.statusIDs = []
|
||||
viewModel.hashtags = []
|
||||
}
|
||||
|
||||
viewModel.userFetchedResultsController.append(userIDs: userIDs)
|
||||
await viewModel.statusFetchedResultsController.appendRecords(statusIDs)
|
||||
|
||||
|
||||
var hashtags = viewModel.hashtags
|
||||
for hashtag in response.value.hashtags where !hashtags.contains(hashtag) {
|
||||
hashtags.append(hashtag)
|
||||
var existingRelationships = viewModel.relationships
|
||||
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 {
|
||||
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()
|
||||
|
||||
|
@ -51,12 +52,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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,6 @@
|
|||
<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="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="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
|
||||
<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="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
|
||||
</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">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
|
@ -236,7 +225,6 @@
|
|||
<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="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 name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
|
||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -271,6 +259,5 @@
|
|||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<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>
|
||||
</model>
|
|
@ -68,7 +68,6 @@ final public class MastodonUser: NSManagedObject {
|
|||
// one-to-many relationship
|
||||
@NSManaged public private(set) var statuses: Set<Status>
|
||||
@NSManaged public private(set) var notifications: Set<Notification>
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
|
||||
// many-to-many relationship
|
||||
@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
|
||||
extension MastodonUser: AutoGenerateProperty {
|
||||
// sourcery:inline:MastodonUser.AutoGenerateProperty
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -98,7 +98,6 @@ public final class Status: NSManagedObject {
|
|||
@NSManaged public private(set) var reblogFrom: Set<Status>
|
||||
@NSManaged public private(set) var replyFrom: Set<Status>
|
||||
@NSManaged public private(set) var notifications: Set<Notification>
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
|
||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
|
|
@ -31,9 +31,6 @@ public final class Tag: NSManagedObject {
|
|||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var followedBy: Set<MastodonUser>
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
}
|
||||
|
||||
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
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -8,7 +8,22 @@
|
|||
|
||||
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 {
|
||||
|
|
|
@ -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…
Reference in New Issue