diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index 98ed7f356..5bc61b648 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -136,6 +136,7 @@
+
@@ -182,8 +183,9 @@
-
-
+
+
+
@@ -234,6 +236,7 @@
+
@@ -266,6 +269,7 @@
+
@@ -277,16 +281,16 @@
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift
index d442ec753..6b27b4cd8 100644
--- a/CoreDataStack/Entity/MastodonUser.swift
+++ b/CoreDataStack/Entity/MastodonUser.swift
@@ -43,6 +43,7 @@ final public class MastodonUser: NSManagedObject {
// one-to-one relationship
@NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
+ @NSManaged public private(set) var searchHistory: SearchHistory?
// one-to-many relationship
@NSManaged public private(set) var statuses: Set?
diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift
index d924917ee..da6d98bc2 100644
--- a/CoreDataStack/Entity/SearchHistory.swift
+++ b/CoreDataStack/Entity/SearchHistory.swift
@@ -13,9 +13,11 @@ public final class SearchHistory: NSManagedObject {
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var createAt: Date
@NSManaged public private(set) var updatedAt: Date
-
+
+ // one-to-one relationship
@NSManaged public private(set) var account: MastodonUser?
@NSManaged public private(set) var hashtag: Tag?
+ @NSManaged public private(set) var status: Status?
}
@@ -51,6 +53,16 @@ extension SearchHistory {
searchHistory.hashtag = hashtag
return searchHistory
}
+
+ @discardableResult
+ public static func insert(
+ into context: NSManagedObjectContext,
+ status: Status
+ ) -> SearchHistory {
+ let searchHistory: SearchHistory = context.insertObject()
+ searchHistory.status = status
+ return searchHistory
+ }
}
public extension SearchHistory {
diff --git a/CoreDataStack/Entity/Status.swift b/CoreDataStack/Entity/Status.swift
index 14f687241..717e54ab7 100644
--- a/CoreDataStack/Entity/Status.swift
+++ b/CoreDataStack/Entity/Status.swift
@@ -38,20 +38,21 @@ public final class Status: NSManagedObject {
@NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code)
@NSManaged public private(set) var text: String?
- // many-to-one relastionship
+ // many-to-one relationship
@NSManaged public private(set) var author: MastodonUser
@NSManaged public private(set) var reblog: Status?
@NSManaged public private(set) var replyTo: Status?
- // many-to-many relastionship
+ // many-to-many relationship
@NSManaged public private(set) var favouritedBy: Set?
@NSManaged public private(set) var rebloggedBy: Set?
@NSManaged public private(set) var mutedBy: Set?
@NSManaged public private(set) var bookmarkedBy: Set?
- // one-to-one relastionship
+ // one-to-one relationship
@NSManaged public private(set) var pinnedBy: MastodonUser?
@NSManaged public private(set) var poll: Poll?
+ @NSManaged public private(set) var searchHistory: SearchHistory?
// one-to-many relationship
@NSManaged public private(set) var reblogFrom: Set?
diff --git a/CoreDataStack/Entity/Tag.swift b/CoreDataStack/Entity/Tag.swift
index 3044cacc0..6aeee520e 100644
--- a/CoreDataStack/Entity/Tag.swift
+++ b/CoreDataStack/Entity/Tag.swift
@@ -17,6 +17,9 @@ public final class Tag: NSManagedObject {
@NSManaged public private(set) var name: String
@NSManaged public private(set) var url: String
+ // one-to-one relationship
+ @NSManaged public private(set) var searchHistory: SearchHistory?
+
// many-to-many relationship
@NSManaged public private(set) var statuses: Set?
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index a389287d7..aded53b0d 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -274,6 +274,10 @@
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; };
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */; };
DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */; };
+ DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */; };
+ DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */; };
+ DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */; };
+ DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */; };
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; };
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; };
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
@@ -921,6 +925,10 @@
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryTableHeaderView.swift; sourceTree = ""; };
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = ""; };
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+StatusProvider.swift"; sourceTree = ""; };
+ DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = ""; };
+ DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = ""; };
+ DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryItem.swift; sourceTree = ""; };
+ DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = ""; };
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = ""; };
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = ""; };
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; };
@@ -1509,6 +1517,7 @@
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
+ DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
);
path = Service;
sourceTree = "";
@@ -1570,24 +1579,13 @@
2D76319D25C151F600929FB9 /* Section */ = {
isa = PBXGroup;
children = (
- 2D76319E25C1521200929FB9 /* StatusSection.swift */,
- DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
- DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
- DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
- 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
- 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
+ DB4F097926A039C400D62E92 /* Status */,
+ DB4F097826A039B400D62E92 /* Onboarding */,
+ DB4F097726A039A200D62E92 /* Search */,
+ DB4F097626A0398000D62E92 /* Compose */,
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
- 2D35237926256D920031AF25 /* NotificationSection.swift */,
- 2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
- DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
- DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
- DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
- DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
- 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
- DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
- DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
);
path = Section;
sourceTree = "";
@@ -1641,6 +1639,7 @@
children = (
2D7631B225C159F700929FB9 /* Item.swift */,
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
+ DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */,
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
2D7867182625B77500211898 /* NotificationItem.swift */,
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
@@ -2010,7 +2009,6 @@
DB4F0964269ED06700D62E92 /* SearchResult */ = {
isa = PBXGroup;
children = (
- 2DFAD5212616F8E300F9EE7C /* TableViewCell */,
DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */,
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */,
DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */,
@@ -2019,6 +2017,57 @@
path = SearchResult;
sourceTree = "";
};
+ DB4F097626A0398000D62E92 /* Compose */ = {
+ isa = PBXGroup;
+ children = (
+ DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
+ DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
+ DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
+ DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
+ DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
+ );
+ path = Compose;
+ sourceTree = "";
+ };
+ DB4F097726A039A200D62E92 /* Search */ = {
+ isa = PBXGroup;
+ children = (
+ 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
+ 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
+ 2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
+ DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */,
+ );
+ path = Search;
+ sourceTree = "";
+ };
+ DB4F097826A039B400D62E92 /* Onboarding */ = {
+ isa = PBXGroup;
+ children = (
+ DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
+ DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
+ );
+ path = Onboarding;
+ sourceTree = "";
+ };
+ DB4F097926A039C400D62E92 /* Status */ = {
+ isa = PBXGroup;
+ children = (
+ 2D76319E25C1521200929FB9 /* StatusSection.swift */,
+ DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
+ 2D35237926256D920031AF25 /* NotificationSection.swift */,
+ 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
+ );
+ path = Status;
+ sourceTree = "";
+ };
+ DB4F098026A0475500D62E92 /* View */ = {
+ isa = PBXGroup;
+ children = (
+ DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */,
+ );
+ path = View;
+ sourceTree = "";
+ };
DB4FFC2D269EC39C00D62E92 /* Search */ = {
isa = PBXGroup;
children = (
@@ -2428,8 +2477,6 @@
children = (
DBF1D253269DB02C00C1C08A /* Search */,
DBF1D24F269DAF6100C1C08A /* SearchDetail */,
- DB4F0964269ED06700D62E92 /* SearchResult */,
- DBF1D252269DB01700C1C08A /* SearchHistory */,
);
path = Search;
sourceTree = "";
@@ -2656,6 +2703,7 @@
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */,
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
+ DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */,
);
path = FetchedResultsController;
sourceTree = "";
@@ -2696,6 +2744,9 @@
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
isa = PBXGroup;
children = (
+ 2DFAD5212616F8E300F9EE7C /* TableViewCell */,
+ DB4F0964269ED06700D62E92 /* SearchResult */,
+ DBF1D252269DB01700C1C08A /* SearchHistory */,
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */,
);
@@ -2705,8 +2756,9 @@
DBF1D252269DB01700C1C08A /* SearchHistory */ = {
isa = PBXGroup;
children = (
+ DB4F098026A0475500D62E92 /* View */,
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */,
- DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */,
+ DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */,
);
path = SearchHistory;
sourceTree = "";
@@ -3388,6 +3440,7 @@
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */,
+ DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */,
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
@@ -3528,6 +3581,7 @@
DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */,
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
+ DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
@@ -3555,6 +3609,7 @@
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */,
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
+ DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */,
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */,
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
@@ -3624,6 +3679,7 @@
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
+ DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */,
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
diff --git a/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift b/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift
new file mode 100644
index 000000000..b83bfe662
--- /dev/null
+++ b/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift
@@ -0,0 +1,53 @@
+//
+// SearchHistoryFetchedResultController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-15.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+final class SearchHistoryFetchedResultController: NSObject {
+
+ var disposeBag = Set()
+
+ let fetchedResultsController: NSFetchedResultsController
+
+ // output
+ let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
+
+ 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()
+
+ fetchedResultsController.delegate = self
+ }
+
+}
+
+// MARK: - NSFetchedResultsControllerDelegate
+extension SearchHistoryFetchedResultController: NSFetchedResultsControllerDelegate {
+ func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ let objects = fetchedResultsController.fetchedObjects ?? []
+ self.objectIDs.value = objects.map { $0.objectID }
+ }
+}
diff --git a/Mastodon/Diffiable/Item/SearchHistoryItem.swift b/Mastodon/Diffiable/Item/SearchHistoryItem.swift
new file mode 100644
index 000000000..de97eae34
--- /dev/null
+++ b/Mastodon/Diffiable/Item/SearchHistoryItem.swift
@@ -0,0 +1,41 @@
+//
+// SearchHistoryItem.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-15.
+//
+
+import Foundation
+import CoreData
+
+enum SearchHistoryItem {
+ case account(objectID: NSManagedObjectID)
+ case hashtag(objectID: NSManagedObjectID)
+ case status(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
+}
+
+extension SearchHistoryItem: Hashable {
+ static func == (lhs: SearchHistoryItem, rhs: SearchHistoryItem) -> Bool {
+ switch (lhs, rhs) {
+ case (.account(let objectIDLeft), account(let objectIDRight)):
+ return objectIDLeft == objectIDRight
+ case (.hashtag(let objectIDLeft), hashtag(let objectIDRight)):
+ return objectIDLeft == objectIDRight
+ case (.status(let objectIDLeft, _), status(let objectIDRight, _)):
+ return objectIDLeft == objectIDRight
+ default:
+ return false
+ }
+ }
+
+ func hash(into hasher: inout Hasher) {
+ switch self {
+ case .account(let objectID):
+ hasher.combine(objectID)
+ case .hashtag(let objectID):
+ hasher.combine(objectID)
+ case .status(let objectID, _):
+ hasher.combine(objectID)
+ }
+ }
+}
diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift
index b3d70be49..7f57c4355 100644
--- a/Mastodon/Diffiable/Item/SearchResultItem.swift
+++ b/Mastodon/Diffiable/Item/SearchResultItem.swift
@@ -12,11 +12,7 @@ import MastodonSDK
enum SearchResultItem {
case hashtag(tag: Mastodon.Entity.Tag)
case account(account: Mastodon.Entity.Account)
-
- case accountObjectID(accountObjectID: NSManagedObjectID)
- case hashtagObjectID(hashtagObjectID: NSManagedObjectID)
case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute)
-
case bottomLoader(attribute: BottomLoaderAttribute)
}
@@ -47,10 +43,6 @@ extension SearchResultItem: Equatable {
return tagLeft == tagRight
case (.account(let accountLeft), .account(let accountRight)):
return accountLeft == accountRight
- case (.accountObjectID(let idLeft), .accountObjectID(let idRight)):
- return idLeft == idRight
- case (.hashtagObjectID(let idLeft), .hashtagObjectID(let idRight)):
- return idLeft == idRight
case (.status(let idLeft, _), .status(let idRight, _)):
return idLeft == idRight
case (.bottomLoader(let attributeLeft), .bottomLoader(let attributeRight)):
@@ -70,10 +62,6 @@ extension SearchResultItem: Hashable {
case .hashtag(let tag):
hasher.combine(String(describing: SearchResultItem.hashtag.self))
hasher.combine(tag.name)
- case .accountObjectID(let id):
- hasher.combine(id)
- case .hashtagObjectID(let id):
- hasher.combine(id)
case .status(let id, _):
hasher.combine(id)
case .bottomLoader(let attribute):
@@ -99,8 +87,6 @@ extension SearchResultItem {
return .status(objectID: objectID)
case .hashtag,
.account,
- .accountObjectID,
- .hashtagObjectID,
.bottomLoader:
return nil
}
diff --git a/Mastodon/Diffiable/Section/AutoCompleteSection.swift b/Mastodon/Diffiable/Section/Compose/AutoCompleteSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/AutoCompleteSection.swift
rename to Mastodon/Diffiable/Section/Compose/AutoCompleteSection.swift
diff --git a/Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusAttachmentSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift
rename to Mastodon/Diffiable/Section/Compose/ComposeStatusAttachmentSection.swift
diff --git a/Mastodon/Diffiable/Section/ComposeStatusPollSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusPollSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/ComposeStatusPollSection.swift
rename to Mastodon/Diffiable/Section/Compose/ComposeStatusPollSection.swift
diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/ComposeStatusSection.swift
rename to Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift
diff --git a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift
rename to Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift
diff --git a/Mastodon/Diffiable/Section/CategoryPickerSection.swift b/Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/CategoryPickerSection.swift
rename to Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift
diff --git a/Mastodon/Diffiable/Section/PickServerSection.swift b/Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/PickServerSection.swift
rename to Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift
diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/RecommendAccountSection.swift
rename to Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift
diff --git a/Mastodon/Diffiable/Section/RecommendHashTagSection.swift b/Mastodon/Diffiable/Section/Search/RecommendHashTagSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/RecommendHashTagSection.swift
rename to Mastodon/Diffiable/Section/Search/RecommendHashTagSection.swift
diff --git a/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift b/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift
new file mode 100644
index 000000000..8f39eb6bd
--- /dev/null
+++ b/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift
@@ -0,0 +1,56 @@
+//
+// SearchHistorySection.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-15.
+//
+
+import UIKit
+import CoreDataStack
+
+enum SearchHistorySection: Hashable {
+ case main
+}
+
+extension SearchHistorySection {
+ static func tableViewDiffableDataSource(
+ for tableView: UITableView,
+ dependency: NeedsDependency
+ ) -> UITableViewDiffableDataSource {
+ UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
+ switch item {
+ case .account(let objectID):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
+ if let user = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? MastodonUser {
+ cell.config(with: user)
+ }
+ return cell
+ case .hashtag(let objectID):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell
+ if let hashtag = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? Tag {
+ cell.config(with: hashtag)
+ }
+ return cell
+ case .status:
+ return UITableViewCell()
+// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
+// if let status = try? dependency.context.managedObjectContext.existingObject(with: statusObjectID) as? Status {
+// let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
+// let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
+// StatusSection.configure(
+// cell: cell,
+// tableView: tableView,
+// timelineContext: .search,
+// dependency: dependency,
+// readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
+// status: status,
+// requestUserID: requestUserID,
+// statusItemAttribute: attribute
+// )
+// }
+// cell.delegate = statusTableViewCellDelegate
+// return cell
+ } // end switch
+ } // end UITableViewDiffableDataSource
+ } // end func
+}
diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/Search/SearchResultSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/SearchResultSection.swift
rename to Mastodon/Diffiable/Section/Search/SearchResultSection.swift
diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/NotificationSection.swift
rename to Mastodon/Diffiable/Section/Status/NotificationSection.swift
diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/Status/PollSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/PollSection.swift
rename to Mastodon/Diffiable/Section/Status/PollSection.swift
diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/Status/ReportSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/ReportSection.swift
rename to Mastodon/Diffiable/Section/Status/ReportSection.swift
diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/Status/StatusSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/StatusSection.swift
rename to Mastodon/Diffiable/Section/Status/StatusSection.swift
diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift
index e3b551628..681fa0f54 100644
--- a/Mastodon/Scene/Search/Search/SearchViewModel.swift
+++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift
@@ -25,13 +25,7 @@ final class SearchViewModel: NSObject {
let viewDidAppeared = PassthroughSubject()
// output
- let searchText = CurrentValueSubject("")
- let searchScope = CurrentValueSubject(Mastodon.API.V2.Search.SearchType.default)
-
- let isSearching = CurrentValueSubject(false)
-
- let searchResult = CurrentValueSubject(nil)
-
+
// var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject()
@@ -39,85 +33,11 @@ final class SearchViewModel: NSObject {
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource?
var accountDiffableDataSource: UICollectionViewDiffableDataSource?
- let statusFetchedResultsController: StatusFetchedResultsController
-
init(context: AppContext, coordinator: SceneCoordinator) {
self.coordinator = coordinator
self.context = context
- self.statusFetchedResultsController = StatusFetchedResultsController(
- managedObjectContext: context.managedObjectContext,
- domain: nil,
- additionalTweetPredicate: nil
- )
super.init()
- // bind active authentication
- context.authenticationService.activeMastodonAuthentication
- .sink { [weak self] activeMastodonAuthentication in
- guard let self = self else { return }
- guard let activeMastodonAuthentication = activeMastodonAuthentication else {
- self.currentMastodonUser.value = nil
- return
- }
- self.currentMastodonUser.value = activeMastodonAuthentication.user
- self.statusFetchedResultsController.domain.value = activeMastodonAuthentication.domain
- }
- .store(in: &disposeBag)
-
- Publishers.CombineLatest(
- searchText
- .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
- searchScope
- )
- .filter { text, _ in
- !text.isEmpty
- }
- .compactMap { (text, scope) -> AnyPublisher, Error>, Never>? in
- guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
- let query = Mastodon.API.V2.Search.Query(
- q: text,
- type: scope,
- accountID: nil,
- maxID: nil,
- minID: nil,
- excludeUnreviewed: nil,
- resolve: nil,
- limit: nil,
- offset: nil,
- following: nil
- )
- return context.apiService.search(
- domain: activeMastodonAuthenticationBox.domain,
- query: query,
- mastodonAuthenticationBox: activeMastodonAuthenticationBox
- )
- // .retry(3) // iOS 14.0 SDK may not works here. needs testing before add this
- .map { response in Result, Error> { response } }
- .catch { error in Just(Result, Error> { throw error }) }
- .eraseToAnyPublisher()
- }
- .switchToLatest()
- .sink { [weak self] result in
- guard let self = self else { return }
- switch result {
- case .success(let response):
- guard self.isSearching.value else { return }
- self.searchResult.value = response.value
- case .failure(let error):
- break
- }
- }
- .store(in: &disposeBag)
-
- isSearching
- .sink { [weak self] isSearching in
- if !isSearching {
- self?.searchResult.value = nil
- self?.searchText.value = ""
- }
- }
- .store(in: &disposeBag)
-
Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared
@@ -220,126 +140,5 @@ final class SearchViewModel: NSObject {
snapshot.appendItems(self.recommendAccounts, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
-
- func searchResultItemDidSelected(item: SearchResultItem, from: UIViewController) {
- let searchHistories = fetchSearchHistory()
- _ = context.managedObjectContext.performChanges { [weak self] in
- guard let self = self else { return }
- switch item {
- case .account(let account):
- guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
- return
- }
- // load request mastodon user
- let requestMastodonUser: MastodonUser? = {
- let request = MastodonUser.sortedFetchRequest
- request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, id: activeMastodonAuthenticationBox.userID)
- request.fetchLimit = 1
- request.returnsObjectsAsFaults = false
- do {
- return try self.context.managedObjectContext.fetch(request).first
- } catch {
- assertionFailure(error.localizedDescription)
- return nil
- }
- }()
- let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.context.managedObjectContext, for: requestMastodonUser, in: activeMastodonAuthenticationBox.domain, entity: account, userCache: nil, networkDate: Date(), log: OSLog.api)
- if let searchHistories = searchHistories {
- let history = searchHistories.first { history -> Bool in
- guard let account = history.account else { return false }
- return account.objectID == mastodonUser.objectID
- }
- if let history = history {
- history.update(updatedAt: Date())
- } else {
- SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser)
- }
- } else {
- SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser)
- }
- let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: mastodonUser)
- DispatchQueue.main.async {
- self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show)
- }
- case .hashtag(let tag):
- let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag)
- if let searchHistories = searchHistories {
- let history = searchHistories.first { history -> Bool in
- guard let hashtag = history.hashtag else { return false }
- return hashtag.objectID == tagInCoreData.objectID
- }
- if let history = history {
- history.update(updatedAt: Date())
- } else {
- SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData)
- }
- } else {
- SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData)
- }
- let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name)
- DispatchQueue.main.async {
- self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show)
- }
- case .accountObjectID(let accountObjectID):
- if let searchHistories = searchHistories {
- let history = searchHistories.first { history -> Bool in
- guard let account = history.account else { return false }
- return account.objectID == accountObjectID
- }
- if let history = history {
- history.update(updatedAt: Date())
- }
- }
- let mastodonUser = self.context.managedObjectContext.object(with: accountObjectID) as! MastodonUser
- let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: mastodonUser)
- DispatchQueue.main.async {
- self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show)
- }
- case .hashtagObjectID(let hashtagObjectID):
- if let searchHistories = searchHistories {
- let history = searchHistories.first { history -> Bool in
- guard let hashtag = history.hashtag else { return false }
- return hashtag.objectID == hashtagObjectID
- }
- if let history = history {
- history.update(updatedAt: Date())
- }
- }
- let tagInCoreData = self.context.managedObjectContext.object(with: hashtagObjectID) as! Tag
- let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name)
- DispatchQueue.main.async {
- self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show)
- }
- default:
- break
- }
- }
- }
-
- func fetchSearchHistory() -> [SearchHistory]? {
- let searchHistory: [SearchHistory]? = {
- let request = SearchHistory.sortedFetchRequest
- request.predicate = nil
- request.returnsObjectsAsFaults = false
- do {
- return try context.managedObjectContext.fetch(request)
- } catch {
- assertionFailure(error.localizedDescription)
- return nil
- }
-
- }()
- return searchHistory
- }
-
- func deleteSearchHistory() {
- let result = fetchSearchHistory()
- _ = context.managedObjectContext.performChanges { [weak self] in
- result?.forEach { history in
- self?.context.managedObjectContext.delete(history)
- }
- self?.isSearching.value = true
- }
- }
}
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift
index 2505eb42a..02880b796 100644
--- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift
+++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift
@@ -23,6 +23,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
var viewModel: SearchDetailViewModel!
var viewControllers: [SearchResultViewController]!
+ let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
let navigationBarBackgroundView = UIView()
let navigationBar: UINavigationBar = {
let navigationItem = UINavigationItem()
@@ -32,7 +33,9 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
navigationItem.compactAppearance = barAppearance
navigationItem.scrollEdgeAppearance = barAppearance
- let navigationBar = UINavigationBar()
+ let navigationBar = UINavigationBar(
+ frame: CGRect(x: 0, y: 0, width: 300, height: 100)
+ )
navigationBar.setItems([navigationItem], animated: false)
return navigationBar
}()
@@ -40,9 +43,18 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
let searchBar = UISearchBar()
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
+ searchBar.sizeToFit()
searchBar.scopeBarBackgroundImage = UIImage()
return searchBar
}()
+
+ private(set) lazy var searchHistoryViewController: SearchHistoryViewController = {
+ let searchHistoryViewController = SearchHistoryViewController()
+ searchHistoryViewController.context = context
+ searchHistoryViewController.coordinator = coordinator
+ searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context)
+ return searchHistoryViewController
+ }()
}
extension SearchDetailViewController {
@@ -82,6 +94,26 @@ extension SearchDetailViewController {
navigationBarBackgroundView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
])
+ navigationBarVisualEffectBackgroundView.translatesAutoresizingMaskIntoConstraints = false
+ view.insertSubview(navigationBarVisualEffectBackgroundView, belowSubview: navigationBarBackgroundView)
+ NSLayoutConstraint.activate([
+ navigationBarVisualEffectBackgroundView.topAnchor.constraint(equalTo: navigationBarBackgroundView.topAnchor),
+ navigationBarVisualEffectBackgroundView.leadingAnchor.constraint(equalTo: navigationBarBackgroundView.leadingAnchor),
+ navigationBarVisualEffectBackgroundView.trailingAnchor.constraint(equalTo: navigationBarBackgroundView.trailingAnchor),
+ navigationBarVisualEffectBackgroundView.bottomAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
+ ])
+
+ addChild(searchHistoryViewController)
+ searchHistoryViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(searchHistoryViewController.view)
+ searchHistoryViewController.didMove(toParent: self)
+ NSLayoutConstraint.activate([
+ searchHistoryViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
+ searchHistoryViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ searchHistoryViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ searchHistoryViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ ])
+
transition = Transition(style: .fade, duration: 0.1)
isScrollEnabled = false
@@ -168,12 +200,25 @@ extension SearchDetailViewController {
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
}
.store(in: &disposeBag)
+
+ // bind search history display
+ viewModel.searchText
+ .removeDuplicates()
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] searchText in
+ guard let self = self else { return }
+ self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
+ }
+ .store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
+ searchBar.setShowsScope(true, animated: false)
+ searchBar.setNeedsLayout()
+ searchBar.layoutIfNeeded()
}
override func viewWillDisappear(_ animated: Bool) {
@@ -193,11 +238,7 @@ extension SearchDetailViewController {
extension SearchDetailViewController {
private func setupSearchBar() {
- searchBar.setShowsScope(true, animated: false)
- searchBar.sizeToFit()
-
navigationBar.topItem?.titleView = searchBar
- navigationBar.sizeToFit()
searchBar.delegate = self
}
@@ -222,7 +263,7 @@ extension SearchDetailViewController: UISearchBarDelegate {
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
- navigationController?.popViewController(animated: true)
+ navigationController?.popViewController(animated: false)
}
}
@@ -231,7 +272,7 @@ extension SearchDetailViewController: UISearchBarDelegate {
extension SearchDetailViewController: PageboyViewControllerDataSource {
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
- return 4
+ return viewControllers.count
}
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift
new file mode 100644
index 000000000..f60b2029d
--- /dev/null
+++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift
@@ -0,0 +1,128 @@
+//
+// SearchHistoryViewController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-13.
+//
+
+import UIKit
+import Combine
+import CoreDataStack
+
+final class SearchHistoryViewController: UIViewController, NeedsDependency {
+
+ var disposeBag = Set()
+
+ weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
+ weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+
+ var viewModel: SearchHistoryViewModel!
+
+ let searchHistoryTableHeaderView = SearchHistoryTableHeaderView()
+ let tableView: UITableView = {
+ let tableView = UITableView()
+ tableView.register(SearchResultTableViewCell.self, forCellReuseIdentifier: String(describing: SearchResultTableViewCell.self))
+// tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
+ tableView.separatorStyle = .none
+ tableView.tableFooterView = UIView()
+ tableView.backgroundColor = .clear
+ return tableView
+ }()
+
+}
+
+extension SearchHistoryViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
+ ThemeService.shared.currentTheme
+ .receive(on: RunLoop.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.setupBackgroundColor(theme: theme)
+ }
+ .store(in: &disposeBag)
+
+ tableView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(tableView)
+ NSLayoutConstraint.activate([
+ tableView.topAnchor.constraint(equalTo: view.topAnchor),
+ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ ])
+
+ tableView.delegate = self
+ viewModel.setupDiffableDataSource(
+ tableView: tableView,
+ dependency: self
+ )
+
+ searchHistoryTableHeaderView.delegate = self
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ tableView.deselectRow(with: transitionCoordinator, animated: animated)
+ }
+
+}
+
+extension SearchHistoryViewController {
+ private func setupBackgroundColor(theme: Theme) {
+ view.backgroundColor = theme.systemGroupedBackgroundColor
+ }
+}
+
+// MARK: - UITableViewDelegate
+extension SearchHistoryViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+ switch section {
+ case 0:
+ return searchHistoryTableHeaderView
+ default:
+ return UIView()
+ }
+ }
+
+ func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+ switch section {
+ case 0:
+ return UITableView.automaticDimension
+ default:
+ return .leastNonzeroMagnitude
+ }
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ guard let diffableDataSource = viewModel.diffableDataSource else { return }
+ guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
+
+ viewModel.persistSearchHistory(for: item)
+
+ switch item {
+ case .account(let objectID):
+ guard let user = try? viewModel.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return }
+ let profileViewModel = CachedProfileViewModel(context: context, mastodonUser: user)
+ coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show)
+ case .hashtag(let objectID):
+ guard let hashtag = try? viewModel.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? Tag else { return }
+ let hashtagViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name)
+ coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: self, transition: .show)
+ case .status(let objectID, _):
+ guard let status = try? viewModel.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? Status else { return }
+ let threadViewModel = CachedThreadViewModel(context: context, status: status)
+ coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show)
+ }
+ }
+}
+
+// MARK: - SearchHistoryTableHeaderViewDelegate
+extension SearchHistoryViewController: SearchHistoryTableHeaderViewDelegate {
+ func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton) {
+ viewModel.clearSearchHistory()
+ }
+}
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift
new file mode 100644
index 000000000..19bb875c9
--- /dev/null
+++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift
@@ -0,0 +1,132 @@
+//
+// SearchHistoryViewModel.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-15.
+//
+
+import UIKit
+import Combine
+import CoreDataStack
+import CommonOSLog
+
+final class SearchHistoryViewModel {
+
+ var disposeBag = Set()
+
+ // input
+ let context: AppContext
+ let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
+
+ // output
+ var diffableDataSource: UITableViewDiffableDataSource!
+
+ init(context: AppContext) {
+ self.context = context
+ self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext)
+
+ // may block main queue by large dataset
+ searchHistoryFetchedResultController.objectIDs
+ .removeDuplicates()
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] objectIDs in
+ guard let self = self else { return }
+ guard let diffableDataSource = self.diffableDataSource else { return }
+ let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext
+
+ var items: [SearchHistoryItem] = []
+ for objectID in objectIDs {
+ guard let searchHistory = try? managedObjectContext.existingObject(with: objectID) as? SearchHistory else { continue }
+ if let account = searchHistory.account {
+ items.append(.account(objectID: account.objectID))
+ } else if let hashtag = searchHistory.hashtag {
+ items.append(.hashtag(objectID: hashtag.objectID))
+ } else {
+ // TODO: status
+ }
+ }
+
+ var snapshot = NSDiffableDataSourceSnapshot()
+ snapshot.appendSections([.main])
+ snapshot.appendItems(items, toSection: .main)
+
+ diffableDataSource.apply(snapshot, animatingDifferences: false)
+ }
+ .store(in: &disposeBag)
+
+ try? searchHistoryFetchedResultController.fetchedResultsController.performFetch()
+ }
+
+}
+
+extension SearchHistoryViewModel {
+ func setupDiffableDataSource(
+ tableView: UITableView,
+ dependency: NeedsDependency
+ ) {
+ diffableDataSource = SearchHistorySection.tableViewDiffableDataSource(
+ for: tableView,
+ dependency: dependency
+ )
+
+ var snapshot = NSDiffableDataSourceSnapshot()
+ snapshot.appendSections([.main])
+ diffableDataSource.apply(snapshot, animatingDifferences: false)
+ }
+}
+
+extension SearchHistoryViewModel {
+ func persistSearchHistory(for item: SearchHistoryItem) {
+ switch item {
+ case .account(let objectID):
+ let managedObjectContext = context.backgroundManagedObjectContext
+ managedObjectContext.performChanges {
+ guard let user = try? managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return }
+ if let searchHistory = user.searchHistory {
+ searchHistory.update(updatedAt: Date())
+ } else {
+ SearchHistory.insert(into: managedObjectContext, account: user)
+ }
+ }
+ .sink { result in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
+
+ case .hashtag(let objectID):
+ let managedObjectContext = context.backgroundManagedObjectContext
+ managedObjectContext.performChanges {
+ guard let hashtag = try? managedObjectContext.existingObject(with: objectID) as? Tag else { return }
+ if let searchHistory = hashtag.searchHistory {
+ searchHistory.update(updatedAt: Date())
+ } else {
+ SearchHistory.insert(into: managedObjectContext, hashtag: hashtag)
+ }
+ }
+ .sink { result in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
+
+ case .status:
+ // FIXME:
+ break
+ }
+ }
+
+ func clearSearchHistory() {
+ let managedObjectContext = context.backgroundManagedObjectContext
+ managedObjectContext.performChanges {
+ let request = SearchHistory.sortedFetchRequest
+ let searchHistories = managedObjectContext.safeFetch(request)
+ for searchHistory in searchHistories {
+ managedObjectContext.delete(searchHistory)
+ }
+ }
+ .sink { result in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
+
+ }
+}
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift
new file mode 100644
index 000000000..6a360e78b
--- /dev/null
+++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift
@@ -0,0 +1,96 @@
+//
+// SearchHistoryTableHeaderView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-14.
+//
+
+import os.log
+import UIKit
+import Combine
+
+protocol SearchHistoryTableHeaderViewDelegate: AnyObject {
+ func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton)
+}
+
+final class SearchHistoryTableHeaderView: UIView {
+
+ let logger = Logger(subsystem: "SearchHistory", category: "UI")
+
+ weak var delegate: SearchHistoryTableHeaderViewDelegate?
+ var disposeBag = Set()
+
+ let recentSearchesLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
+ label.textColor = Asset.Colors.Label.primary.color
+ label.text = L10n.Scene.Search.Searching.recentSearch
+ return label
+ }()
+
+ let clearSearchHistoryButton: HighlightDimmableButton = {
+ let button = HighlightDimmableButton(type: .custom)
+ button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
+ button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
+ button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal)
+ return button
+ }()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+}
+
+extension SearchHistoryTableHeaderView {
+ private func _init() {
+ preservesSuperviewLayoutMargins = true
+
+ recentSearchesLabel.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(recentSearchesLabel)
+ NSLayoutConstraint.activate([
+ recentSearchesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16),
+ recentSearchesLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
+ bottomAnchor.constraint(equalTo: recentSearchesLabel.bottomAnchor, constant: 16),
+ ])
+
+ clearSearchHistoryButton.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(clearSearchHistoryButton)
+ NSLayoutConstraint.activate([
+ clearSearchHistoryButton.centerYAnchor.constraint(equalTo: recentSearchesLabel.centerYAnchor),
+ clearSearchHistoryButton.leadingAnchor.constraint(equalTo: recentSearchesLabel.trailingAnchor),
+ clearSearchHistoryButton.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
+ ])
+ clearSearchHistoryButton.setContentHuggingPriority(.defaultHigh + 10, for: .horizontal)
+
+ clearSearchHistoryButton.addTarget(self, action: #selector(SearchHistoryTableHeaderView.clearSearchHistoryButtonDidPressed(_:)), for: .touchUpInside)
+
+ setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
+ ThemeService.shared.currentTheme
+ .receive(on: RunLoop.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.setupBackgroundColor(theme: theme)
+ }
+ .store(in: &disposeBag)
+ }
+}
+
+extension SearchHistoryTableHeaderView {
+ @objc private func clearSearchHistoryButtonDidPressed(_ sender: UIButton) {
+ logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
+ delegate?.searchHistoryTableHeaderView(self, clearSearchHistoryButtonDidPressed: sender)
+ }
+}
+
+extension SearchHistoryTableHeaderView {
+ private func setupBackgroundColor(theme: Theme) {
+ backgroundColor = theme.systemGroupedBackgroundColor
+ }
+}
diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewController+StatusProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+StatusProvider.swift
similarity index 100%
rename from Mastodon/Scene/Search/SearchResult/SearchResultViewController+StatusProvider.swift
rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+StatusProvider.swift
diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift
similarity index 98%
rename from Mastodon/Scene/Search/SearchResult/SearchResultViewController.swift
rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift
index 988c2945b..6c320af51 100644
--- a/Mastodon/Scene/Search/SearchResult/SearchResultViewController.swift
+++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift
@@ -195,7 +195,10 @@ extension SearchResultViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
- let item = diffableDataSource.itemIdentifier(for: indexPath)
+ guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
+
+ viewModel.persistSearchHistory(for: item)
+
switch item {
case .account(let account):
let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id)
@@ -207,8 +210,6 @@ extension SearchResultViewController: UITableViewDelegate {
aspectTableView(tableView, didSelectRowAt: indexPath)
case .bottomLoader:
break
- default:
- assertionFailure()
}
}
diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift
similarity index 100%
rename from Mastodon/Scene/Search/SearchResult/SearchResultViewModel+State.swift
rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift
diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
similarity index 72%
rename from Mastodon/Scene/Search/SearchResult/SearchResultViewModel.swift
rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
index 1b5cb504a..0ace96226 100644
--- a/Mastodon/Scene/Search/SearchResult/SearchResultViewModel.swift
+++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
@@ -10,6 +10,7 @@ import Combine
import CoreData
import CoreDataStack
import GameplayKit
+import CommonOSLog
final class SearchResultViewModel {
@@ -137,3 +138,59 @@ extension SearchResultViewModel {
diffableDataSource.apply(snapshot, animatingDifferences: false)
}
}
+
+extension SearchResultViewModel {
+ func persistSearchHistory(for item: SearchResultItem) {
+ guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
+ let domain = box.domain
+
+ switch item {
+ case .account(let account):
+ let managedObjectContext = context.backgroundManagedObjectContext
+ managedObjectContext.performChanges {
+ let (user, _) = APIService.CoreData.createOrMergeMastodonUser(
+ into: managedObjectContext,
+ for: nil,
+ in: domain,
+ entity: account,
+ userCache: nil,
+ networkDate: Date(),
+ log: OSLog.api
+ )
+ if let searchHistory = user.searchHistory {
+ searchHistory.update(updatedAt: Date())
+ } else {
+ SearchHistory.insert(into: managedObjectContext, account: user)
+ }
+ }
+ .sink { result in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
+
+ case .hashtag(let hashtag):
+ let managedObjectContext = context.backgroundManagedObjectContext
+ managedObjectContext.performChanges {
+ let (hashtag, _) = APIService.CoreData.createOrMergeTag(
+ into: managedObjectContext,
+ entity: hashtag
+ )
+ if let searchHistory = hashtag.searchHistory {
+ searchHistory.update(updatedAt: Date())
+ } else {
+ SearchHistory.insert(into: managedObjectContext, hashtag: hashtag)
+ }
+ }
+ .sink { result in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
+
+ case .status:
+ // FIXME:
+ break
+ case .bottomLoader:
+ break
+ }
+ }
+}
diff --git a/Mastodon/Scene/Search/SearchResult/TableViewCell/SearchResultTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift
similarity index 98%
rename from Mastodon/Scene/Search/SearchResult/TableViewCell/SearchResultTableViewCell.swift
rename to Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift
index ca563845a..8223c6ddf 100644
--- a/Mastodon/Scene/Search/SearchResult/TableViewCell/SearchResultTableViewCell.swift
+++ b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift
@@ -68,14 +68,14 @@ extension SearchResultTableViewCell {
containerStackView.axis = .horizontal
containerStackView.distribution = .fill
containerStackView.spacing = 12
- containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 21, bottom: 12, right: 12)
+ containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
containerStackView.isLayoutMarginsRelativeArrangement = true
containerStackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(containerStackView)
NSLayoutConstraint.activate([
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
- containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
- containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
+ containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
diff --git a/Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift b/Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift
deleted file mode 100644
index 64a1b0c5d..000000000
--- a/Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-//
-// SearchHistoryTableHeaderView.swift
-// Mastodon
-//
-// Created by MainasuK Cirno on 2021-7-14.
-//
-
-import Foundation
-
-// lazy var searchHeader: UIView = {
-// let view = UIView()
-// view.frame = CGRect(origin: .zero, size: CGSize(width: searchingTableView.frame.width, height: 56))
-// return view
-// }()
-//
-// let recentSearchesLabel: UILabel = {
-// let label = UILabel()
-// label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
-// label.textColor = Asset.Colors.Label.primary.color
-// label.text = L10n.Scene.Search.Searching.recentSearch
-// return label
-// }()
-//
-// let clearSearchHistoryButton: HighlightDimmableButton = {
-// let button = HighlightDimmableButton(type: .custom)
-// button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
-// button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal)
-// return button
-// }()
diff --git a/Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift
deleted file mode 100644
index 2472b454d..000000000
--- a/Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// SearchHistoryViewController.swift
-// Mastodon
-//
-// Created by MainasuK Cirno on 2021-7-13.
-//
-
-import UIKit
-
-final class SearchHistoryViewController: UIViewController, NeedsDependency {
- weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
- weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
-}
-
-extension SearchHistoryViewController {
-
-}
diff --git a/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift
index c47eec472..f06d04d9a 100644
--- a/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift
+++ b/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift
@@ -15,7 +15,7 @@ final class SearchToSearchDetailViewControllerAnimatedTransitioning: ViewControl
override init(operation: UINavigationController.Operation) {
super.init(operation: operation)
- self.transitionDuration = 0.01
+ self.transitionDuration = 0.2
}
deinit {
@@ -37,7 +37,7 @@ extension SearchToSearchDetailViewControllerAnimatedTransitioning {
}
}
- private func pushTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeInOut) -> UIViewPropertyAnimator {
+ private func pushTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeOut) -> UIViewPropertyAnimator {
guard let toVC = transitionContext.viewController(forKey: .to) as? SearchDetailViewController,
let toView = transitionContext.view(forKey: .to) else {
fatalError()
@@ -46,12 +46,14 @@ extension SearchToSearchDetailViewControllerAnimatedTransitioning {
let toViewEndFrame = transitionContext.finalFrame(for: toVC)
transitionContext.containerView.addSubview(toView)
toView.frame = toViewEndFrame
+ toView.alpha = 0
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve)
animator.addAnimations {
}
animator.addCompletion { position in
+ toView.alpha = 1
transitionContext.completeTransition(true)
}
return animator
diff --git a/Mastodon/Scene/Transition/Search/SearchTransitionController.swift b/Mastodon/Scene/Transition/Search/SearchTransitionController.swift
index ce97742f7..871fd7baf 100644
--- a/Mastodon/Scene/Transition/Search/SearchTransitionController.swift
+++ b/Mastodon/Scene/Transition/Search/SearchTransitionController.swift
@@ -26,4 +26,13 @@ extension SearchTransitionController: UINavigationControllerDelegate {
return nil
}
}
+
+ func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
+ switch viewController {
+ case is SearchDetailViewController:
+ navigationController.interactivePopGestureRecognizer?.isEnabled = false
+ default:
+ navigationController.interactivePopGestureRecognizer?.isEnabled = true
+ }
+ }
}
diff --git a/Mastodon/Diffiable/Section/StatusFilterService.swift b/Mastodon/Service/StatusFilterService.swift
similarity index 100%
rename from Mastodon/Diffiable/Section/StatusFilterService.swift
rename to Mastodon/Service/StatusFilterService.swift