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 */; };
|
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 */,
|
||||||
|
|
|
@ -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
|
@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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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") }
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)) ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
|
@ -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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue