feat: add search history back
This commit is contained in:
parent
acc24b7ef5
commit
647f87744b
|
@ -136,6 +136,7 @@
|
||||||
<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" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
|
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
|
||||||
|
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
|
||||||
<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"/>
|
||||||
<relationship name="votePollOptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
<relationship name="votePollOptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
||||||
<relationship name="votePolls" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
<relationship name="votePolls" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
||||||
|
@ -182,8 +183,9 @@
|
||||||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistory" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistory" inverseEntity="Tag"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistory" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Setting" representedClassName=".Setting" syncable="YES">
|
<entity name="Setting" representedClassName=".Setting" syncable="YES">
|
||||||
<attribute name="appearanceRaw" attributeType="String"/>
|
<attribute name="appearanceRaw" attributeType="String"/>
|
||||||
|
@ -234,6 +236,7 @@
|
||||||
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="replyFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
|
<relationship name="replyFrom" optional="YES" 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="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
|
||||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag"/>
|
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Subscription" representedClassName=".Subscription" syncable="YES">
|
<entity name="Subscription" representedClassName=".Subscription" syncable="YES">
|
||||||
|
@ -266,6 +269,7 @@
|
||||||
<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="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
|
<relationship name="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
|
||||||
|
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
|
||||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
|
@ -277,16 +281,16 @@
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="704"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="719"/>
|
||||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="119"/>
|
||||||
<element name="Setting" positionX="72" positionY="162" width="128" height="164"/>
|
<element name="Setting" positionX="72" positionY="162" width="128" height="164"/>
|
||||||
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
|
<element name="Status" positionX="0" positionY="0" width="128" height="614"/>
|
||||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
||||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="Tag" positionX="0" positionY="0" width="128" height="149"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -43,6 +43,7 @@ final public class MastodonUser: NSManagedObject {
|
||||||
// one-to-one relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var pinnedStatus: Status?
|
@NSManaged public private(set) var pinnedStatus: Status?
|
||||||
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
||||||
|
@NSManaged public private(set) var searchHistory: SearchHistory?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var statuses: Set<Status>?
|
@NSManaged public private(set) var statuses: Set<Status>?
|
||||||
|
|
|
@ -14,8 +14,10 @@ public final class SearchHistory: NSManagedObject {
|
||||||
@NSManaged public private(set) var createAt: Date
|
@NSManaged public private(set) var createAt: Date
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
|
||||||
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var account: MastodonUser?
|
@NSManaged public private(set) var account: MastodonUser?
|
||||||
@NSManaged public private(set) var hashtag: Tag?
|
@NSManaged public private(set) var hashtag: Tag?
|
||||||
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +53,16 @@ extension SearchHistory {
|
||||||
searchHistory.hashtag = hashtag
|
searchHistory.hashtag = hashtag
|
||||||
return searchHistory
|
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 {
|
public extension SearchHistory {
|
||||||
|
|
|
@ -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 language: String? // (ISO 639 Part 1 two-letter language code)
|
||||||
@NSManaged public private(set) var text: String?
|
@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 author: MastodonUser
|
||||||
@NSManaged public private(set) var reblog: Status?
|
@NSManaged public private(set) var reblog: Status?
|
||||||
@NSManaged public private(set) var replyTo: Status?
|
@NSManaged public private(set) var replyTo: Status?
|
||||||
|
|
||||||
// many-to-many relastionship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>?
|
||||||
|
|
||||||
// one-to-one relastionship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
||||||
@NSManaged public private(set) var poll: Poll?
|
@NSManaged public private(set) var poll: Poll?
|
||||||
|
@NSManaged public private(set) var searchHistory: SearchHistory?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var reblogFrom: Set<Status>?
|
@NSManaged public private(set) var reblogFrom: Set<Status>?
|
||||||
|
|
|
@ -17,6 +17,9 @@ public final class Tag: NSManagedObject {
|
||||||
@NSManaged public private(set) var name: String
|
@NSManaged public private(set) var name: String
|
||||||
@NSManaged public private(set) var url: String
|
@NSManaged public private(set) var url: String
|
||||||
|
|
||||||
|
// one-to-one relationship
|
||||||
|
@NSManaged public private(set) var searchHistory: SearchHistory?
|
||||||
|
|
||||||
// many-to-many relationship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var statuses: Set<Status>?
|
@NSManaged public private(set) var statuses: Set<Status>?
|
||||||
|
|
||||||
|
|
|
@ -274,6 +274,10 @@
|
||||||
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; };
|
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; };
|
||||||
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.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 */; };
|
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 */; };
|
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; };
|
||||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; };
|
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; };
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.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 = "<group>"; };
|
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryTableHeaderView.swift; sourceTree = "<group>"; };
|
||||||
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = "<group>"; };
|
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||||
|
DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = "<group>"; };
|
||||||
|
DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryItem.swift; sourceTree = "<group>"; };
|
||||||
|
DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = "<group>"; };
|
||||||
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = "<group>"; };
|
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = "<group>"; };
|
||||||
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = "<group>"; };
|
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = "<group>"; };
|
||||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1509,6 +1517,7 @@
|
||||||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||||
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
|
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
|
||||||
|
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
||||||
);
|
);
|
||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1570,24 +1579,13 @@
|
||||||
2D76319D25C151F600929FB9 /* Section */ = {
|
2D76319D25C151F600929FB9 /* Section */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D76319E25C1521200929FB9 /* StatusSection.swift */,
|
DB4F097926A039C400D62E92 /* Status */,
|
||||||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
DB4F097826A039B400D62E92 /* Onboarding */,
|
||||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
DB4F097726A039A200D62E92 /* Search */,
|
||||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
DB4F097626A0398000D62E92 /* Compose */,
|
||||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
|
||||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
|
||||||
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
|
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
|
||||||
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
|
||||||
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
|
|
||||||
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
|
|
||||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
|
||||||
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
|
||||||
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
||||||
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1641,6 +1639,7 @@
|
||||||
children = (
|
children = (
|
||||||
2D7631B225C159F700929FB9 /* Item.swift */,
|
2D7631B225C159F700929FB9 /* Item.swift */,
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||||
|
DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */,
|
||||||
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
|
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
|
||||||
2D7867182625B77500211898 /* NotificationItem.swift */,
|
2D7867182625B77500211898 /* NotificationItem.swift */,
|
||||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
||||||
|
@ -2010,7 +2009,6 @@
|
||||||
DB4F0964269ED06700D62E92 /* SearchResult */ = {
|
DB4F0964269ED06700D62E92 /* SearchResult */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2DFAD5212616F8E300F9EE7C /* TableViewCell */,
|
|
||||||
DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */,
|
DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */,
|
||||||
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */,
|
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */,
|
||||||
DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */,
|
DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */,
|
||||||
|
@ -2019,6 +2017,57 @@
|
||||||
path = SearchResult;
|
path = SearchResult;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB4F097626A0398000D62E92 /* Compose */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
|
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
|
||||||
|
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
|
||||||
|
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||||
|
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||||
|
);
|
||||||
|
path = Compose;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB4F097726A039A200D62E92 /* Search */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
||||||
|
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||||
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||||
|
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */,
|
||||||
|
);
|
||||||
|
path = Search;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB4F097826A039B400D62E92 /* Onboarding */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||||
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||||
|
);
|
||||||
|
path = Onboarding;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB4F097926A039C400D62E92 /* Status */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D76319E25C1521200929FB9 /* StatusSection.swift */,
|
||||||
|
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
||||||
|
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
||||||
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||||
|
);
|
||||||
|
path = Status;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB4F098026A0475500D62E92 /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB4FFC2D269EC39C00D62E92 /* Search */ = {
|
DB4FFC2D269EC39C00D62E92 /* Search */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2428,8 +2477,6 @@
|
||||||
children = (
|
children = (
|
||||||
DBF1D253269DB02C00C1C08A /* Search */,
|
DBF1D253269DB02C00C1C08A /* Search */,
|
||||||
DBF1D24F269DAF6100C1C08A /* SearchDetail */,
|
DBF1D24F269DAF6100C1C08A /* SearchDetail */,
|
||||||
DB4F0964269ED06700D62E92 /* SearchResult */,
|
|
||||||
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
|
||||||
);
|
);
|
||||||
path = Search;
|
path = Search;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2656,6 +2703,7 @@
|
||||||
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
||||||
DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */,
|
DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */,
|
||||||
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
|
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
|
||||||
|
DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */,
|
||||||
);
|
);
|
||||||
path = FetchedResultsController;
|
path = FetchedResultsController;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2696,6 +2744,9 @@
|
||||||
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
|
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
2DFAD5212616F8E300F9EE7C /* TableViewCell */,
|
||||||
|
DB4F0964269ED06700D62E92 /* SearchResult */,
|
||||||
|
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
||||||
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
|
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */,
|
||||||
DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */,
|
DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */,
|
||||||
);
|
);
|
||||||
|
@ -2705,8 +2756,9 @@
|
||||||
DBF1D252269DB01700C1C08A /* SearchHistory */ = {
|
DBF1D252269DB01700C1C08A /* SearchHistory */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB4F098026A0475500D62E92 /* View */,
|
||||||
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */,
|
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */,
|
||||||
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */,
|
DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = SearchHistory;
|
path = SearchHistory;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3388,6 +3440,7 @@
|
||||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
|
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
|
||||||
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */,
|
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */,
|
||||||
|
DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */,
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||||
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
|
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
|
||||||
|
@ -3528,6 +3581,7 @@
|
||||||
DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */,
|
DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */,
|
||||||
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
|
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
|
@ -3555,6 +3609,7 @@
|
||||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
||||||
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */,
|
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */,
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
||||||
|
DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */,
|
||||||
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */,
|
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */,
|
||||||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,
|
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||||
|
@ -3624,6 +3679,7 @@
|
||||||
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||||
|
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
|
||||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
||||||
DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
|
|
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
let fetchedResultsController: NSFetchedResultsController<SearchHistory>
|
||||||
|
|
||||||
|
// 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<NSFetchRequestResult>, 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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,11 +12,7 @@ import MastodonSDK
|
||||||
enum SearchResultItem {
|
enum SearchResultItem {
|
||||||
case hashtag(tag: Mastodon.Entity.Tag)
|
case hashtag(tag: Mastodon.Entity.Tag)
|
||||||
case account(account: Mastodon.Entity.Account)
|
case account(account: Mastodon.Entity.Account)
|
||||||
|
|
||||||
case accountObjectID(accountObjectID: NSManagedObjectID)
|
|
||||||
case hashtagObjectID(hashtagObjectID: NSManagedObjectID)
|
|
||||||
case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute)
|
||||||
|
|
||||||
case bottomLoader(attribute: BottomLoaderAttribute)
|
case bottomLoader(attribute: BottomLoaderAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,10 +43,6 @@ extension SearchResultItem: Equatable {
|
||||||
return tagLeft == tagRight
|
return tagLeft == tagRight
|
||||||
case (.account(let accountLeft), .account(let accountRight)):
|
case (.account(let accountLeft), .account(let accountRight)):
|
||||||
return accountLeft == 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, _)):
|
case (.status(let idLeft, _), .status(let idRight, _)):
|
||||||
return idLeft == idRight
|
return idLeft == idRight
|
||||||
case (.bottomLoader(let attributeLeft), .bottomLoader(let attributeRight)):
|
case (.bottomLoader(let attributeLeft), .bottomLoader(let attributeRight)):
|
||||||
|
@ -70,10 +62,6 @@ extension SearchResultItem: Hashable {
|
||||||
case .hashtag(let tag):
|
case .hashtag(let tag):
|
||||||
hasher.combine(String(describing: SearchResultItem.hashtag.self))
|
hasher.combine(String(describing: SearchResultItem.hashtag.self))
|
||||||
hasher.combine(tag.name)
|
hasher.combine(tag.name)
|
||||||
case .accountObjectID(let id):
|
|
||||||
hasher.combine(id)
|
|
||||||
case .hashtagObjectID(let id):
|
|
||||||
hasher.combine(id)
|
|
||||||
case .status(let id, _):
|
case .status(let id, _):
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case .bottomLoader(let attribute):
|
case .bottomLoader(let attribute):
|
||||||
|
@ -99,8 +87,6 @@ extension SearchResultItem {
|
||||||
return .status(objectID: objectID)
|
return .status(objectID: objectID)
|
||||||
case .hashtag,
|
case .hashtag,
|
||||||
.account,
|
.account,
|
||||||
.accountObjectID,
|
|
||||||
.hashtagObjectID,
|
|
||||||
.bottomLoader:
|
.bottomLoader:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<SearchHistorySection, SearchHistoryItem> {
|
||||||
|
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
|
||||||
|
}
|
|
@ -25,12 +25,6 @@ final class SearchViewModel: NSObject {
|
||||||
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let searchText = CurrentValueSubject<String, Never>("")
|
|
||||||
let searchScope = CurrentValueSubject<Mastodon.API.V2.Search.SearchType, Never>(Mastodon.API.V2.Search.SearchType.default)
|
|
||||||
|
|
||||||
let isSearching = CurrentValueSubject<Bool, Never>(false)
|
|
||||||
|
|
||||||
let searchResult = CurrentValueSubject<Mastodon.Entity.SearchResult?, Never>(nil)
|
|
||||||
|
|
||||||
// var recommendHashTags = [Mastodon.Entity.Tag]()
|
// var recommendHashTags = [Mastodon.Entity.Tag]()
|
||||||
var recommendAccounts = [NSManagedObjectID]()
|
var recommendAccounts = [NSManagedObjectID]()
|
||||||
|
@ -39,85 +33,11 @@ final class SearchViewModel: NSObject {
|
||||||
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
|
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
|
||||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
||||||
|
|
||||||
let statusFetchedResultsController: StatusFetchedResultsController
|
|
||||||
|
|
||||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
self.context = context
|
self.context = context
|
||||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
|
||||||
managedObjectContext: context.managedObjectContext,
|
|
||||||
domain: nil,
|
|
||||||
additionalTweetPredicate: nil
|
|
||||||
)
|
|
||||||
super.init()
|
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<Result<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> { response } }
|
|
||||||
.catch { error in Just(Result<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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(
|
Publishers.CombineLatest(
|
||||||
context.authenticationService.activeMastodonAuthenticationBox,
|
context.authenticationService.activeMastodonAuthenticationBox,
|
||||||
viewDidAppeared
|
viewDidAppeared
|
||||||
|
@ -221,125 +141,4 @@ final class SearchViewModel: NSObject {
|
||||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||||
var viewModel: SearchDetailViewModel!
|
var viewModel: SearchDetailViewModel!
|
||||||
var viewControllers: [SearchResultViewController]!
|
var viewControllers: [SearchResultViewController]!
|
||||||
|
|
||||||
|
let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||||
let navigationBarBackgroundView = UIView()
|
let navigationBarBackgroundView = UIView()
|
||||||
let navigationBar: UINavigationBar = {
|
let navigationBar: UINavigationBar = {
|
||||||
let navigationItem = UINavigationItem()
|
let navigationItem = UINavigationItem()
|
||||||
|
@ -32,7 +33,9 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||||
navigationItem.compactAppearance = barAppearance
|
navigationItem.compactAppearance = barAppearance
|
||||||
navigationItem.scrollEdgeAppearance = 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)
|
navigationBar.setItems([navigationItem], animated: false)
|
||||||
return navigationBar
|
return navigationBar
|
||||||
}()
|
}()
|
||||||
|
@ -40,9 +43,18 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||||
let searchBar = UISearchBar()
|
let searchBar = UISearchBar()
|
||||||
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
||||||
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
|
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
|
||||||
|
searchBar.sizeToFit()
|
||||||
searchBar.scopeBarBackgroundImage = UIImage()
|
searchBar.scopeBarBackgroundImage = UIImage()
|
||||||
return searchBar
|
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 {
|
extension SearchDetailViewController {
|
||||||
|
@ -82,6 +94,26 @@ extension SearchDetailViewController {
|
||||||
navigationBarBackgroundView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
|
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)
|
transition = Transition(style: .fade, duration: 0.1)
|
||||||
isScrollEnabled = false
|
isScrollEnabled = false
|
||||||
|
|
||||||
|
@ -168,12 +200,25 @@ extension SearchDetailViewController {
|
||||||
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
|
searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||||
|
searchBar.setShowsScope(true, animated: false)
|
||||||
|
searchBar.setNeedsLayout()
|
||||||
|
searchBar.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
@ -193,11 +238,7 @@ extension SearchDetailViewController {
|
||||||
|
|
||||||
extension SearchDetailViewController {
|
extension SearchDetailViewController {
|
||||||
private func setupSearchBar() {
|
private func setupSearchBar() {
|
||||||
searchBar.setShowsScope(true, animated: false)
|
|
||||||
searchBar.sizeToFit()
|
|
||||||
|
|
||||||
navigationBar.topItem?.titleView = searchBar
|
navigationBar.topItem?.titleView = searchBar
|
||||||
navigationBar.sizeToFit()
|
|
||||||
|
|
||||||
searchBar.delegate = self
|
searchBar.delegate = self
|
||||||
}
|
}
|
||||||
|
@ -222,7 +263,7 @@ extension SearchDetailViewController: UISearchBarDelegate {
|
||||||
|
|
||||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
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 {
|
extension SearchDetailViewController: PageboyViewControllerDataSource {
|
||||||
|
|
||||||
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
||||||
return 4
|
return viewControllers.count
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
||||||
|
|
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>!
|
||||||
|
|
||||||
|
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<SearchHistorySection, SearchHistoryItem>()
|
||||||
|
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<SearchHistorySection, SearchHistoryItem>()
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -195,7 +195,10 @@ extension SearchResultViewController: UITableViewDelegate {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
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 {
|
switch item {
|
||||||
case .account(let account):
|
case .account(let account):
|
||||||
let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id)
|
let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id)
|
||||||
|
@ -207,8 +210,6 @@ extension SearchResultViewController: UITableViewDelegate {
|
||||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
break
|
break
|
||||||
default:
|
|
||||||
assertionFailure()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
|
import CommonOSLog
|
||||||
|
|
||||||
final class SearchResultViewModel {
|
final class SearchResultViewModel {
|
||||||
|
|
||||||
|
@ -137,3 +138,59 @@ extension SearchResultViewModel {
|
||||||
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,14 +68,14 @@ extension SearchResultTableViewCell {
|
||||||
containerStackView.axis = .horizontal
|
containerStackView.axis = .horizontal
|
||||||
containerStackView.distribution = .fill
|
containerStackView.distribution = .fill
|
||||||
containerStackView.spacing = 12
|
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.isLayoutMarginsRelativeArrangement = true
|
||||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(containerStackView)
|
contentView.addSubview(containerStackView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||||
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
|
@ -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
|
|
||||||
// }()
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -15,7 +15,7 @@ final class SearchToSearchDetailViewControllerAnimatedTransitioning: ViewControl
|
||||||
override init(operation: UINavigationController.Operation) {
|
override init(operation: UINavigationController.Operation) {
|
||||||
super.init(operation: operation)
|
super.init(operation: operation)
|
||||||
|
|
||||||
self.transitionDuration = 0.01
|
self.transitionDuration = 0.2
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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,
|
guard let toVC = transitionContext.viewController(forKey: .to) as? SearchDetailViewController,
|
||||||
let toView = transitionContext.view(forKey: .to) else {
|
let toView = transitionContext.view(forKey: .to) else {
|
||||||
fatalError()
|
fatalError()
|
||||||
|
@ -46,12 +46,14 @@ extension SearchToSearchDetailViewControllerAnimatedTransitioning {
|
||||||
let toViewEndFrame = transitionContext.finalFrame(for: toVC)
|
let toViewEndFrame = transitionContext.finalFrame(for: toVC)
|
||||||
transitionContext.containerView.addSubview(toView)
|
transitionContext.containerView.addSubview(toView)
|
||||||
toView.frame = toViewEndFrame
|
toView.frame = toViewEndFrame
|
||||||
|
toView.alpha = 0
|
||||||
|
|
||||||
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve)
|
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve)
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
|
|
||||||
}
|
}
|
||||||
animator.addCompletion { position in
|
animator.addCompletion { position in
|
||||||
|
toView.alpha = 1
|
||||||
transitionContext.completeTransition(true)
|
transitionContext.completeTransition(true)
|
||||||
}
|
}
|
||||||
return animator
|
return animator
|
||||||
|
|
|
@ -26,4 +26,13 @@ extension SearchTransitionController: UINavigationControllerDelegate {
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue