Merge pull request #93 from tootsuite/feature/searching
Feature/searching
This commit is contained in:
commit
239b6bac4f
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -141,12 +141,19 @@
|
|||
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
|
||||
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="PrivateNote" representedClassName="PrivateNote" syncable="YES">
|
||||
<entity name="PrivateNote" representedClassName=".PrivateNote" syncable="YES">
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
|
||||
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="SearchHistory" representedClassName=".SearchHistory" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName=".Status" syncable="YES">
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -189,23 +196,25 @@
|
|||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<relationship name="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
|
||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Application" positionX="160" positionY="192" width="128" height="104"/>
|
||||
<element name="Attachment" positionX="72" positionY="162" width="128" height="254"/>
|
||||
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||
<element name="Application" positionX="0" positionY="0" width="128" height="104"/>
|
||||
<element name="Attachment" positionX="0" positionY="0" width="128" height="254"/>
|
||||
<element name="Emoji" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="674"/>
|
||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||
</elements>
|
||||
</model>
|
||||
</model>
|
||||
|
|
|
@ -40,6 +40,26 @@ public extension History {
|
|||
}
|
||||
}
|
||||
|
||||
public extension History {
|
||||
func update(day: Date) {
|
||||
if self.day != day {
|
||||
self.day = day
|
||||
}
|
||||
}
|
||||
|
||||
func update(uses: String) {
|
||||
if self.uses != uses {
|
||||
self.uses = uses
|
||||
}
|
||||
}
|
||||
|
||||
func update(accounts: String) {
|
||||
if self.accounts != accounts {
|
||||
self.accounts = accounts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension History {
|
||||
struct Property {
|
||||
public let day: Date
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// SearchHistory.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class SearchHistory: NSManagedObject {
|
||||
public typealias ID = UUID
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var createAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
@NSManaged public private(set) var account: MastodonUser?
|
||||
@NSManaged public private(set) var hashtag: Tag?
|
||||
|
||||
}
|
||||
|
||||
extension SearchHistory {
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(SearchHistory.identifier))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.createAt))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
|
||||
}
|
||||
|
||||
public override func willSave() {
|
||||
super.willSave()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(SearchHistory.updatedAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
account: MastodonUser
|
||||
) -> SearchHistory {
|
||||
let searchHistory: SearchHistory = context.insertObject()
|
||||
searchHistory.account = account
|
||||
return searchHistory
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
hashtag: Tag
|
||||
) -> SearchHistory {
|
||||
let searchHistory: SearchHistory = context.insertObject()
|
||||
searchHistory.hashtag = hashtag
|
||||
return searchHistory
|
||||
}
|
||||
}
|
||||
|
||||
public extension SearchHistory {
|
||||
func update(updatedAt: Date) {
|
||||
setValue(updatedAt, forKey: #keyPath(SearchHistory.updatedAt))
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistory: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -12,25 +12,33 @@ public final class Tag: NSManagedObject {
|
|||
public typealias ID = UUID
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var createAt: Date
|
||||
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
@NSManaged public private(set) var name: String
|
||||
@NSManaged public private(set) var url: String
|
||||
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var statuses: Set<Status>?
|
||||
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var histories: Set<History>?
|
||||
}
|
||||
|
||||
extension Tag {
|
||||
public override func awakeFromInsert() {
|
||||
public extension Tag {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Tag.identifier))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Tag.createAt))
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Tag.updatedAt))
|
||||
}
|
||||
|
||||
|
||||
override func willSave() {
|
||||
super.willSave()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Tag.updatedAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Tag {
|
||||
|
@ -44,8 +52,8 @@ extension Tag {
|
|||
}
|
||||
}
|
||||
|
||||
extension Tag {
|
||||
public struct Property {
|
||||
public extension Tag {
|
||||
struct Property {
|
||||
public let name: String
|
||||
public let url: String
|
||||
public let histories: [History]?
|
||||
|
@ -58,8 +66,36 @@ extension Tag {
|
|||
}
|
||||
}
|
||||
|
||||
extension Tag: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)]
|
||||
public extension Tag {
|
||||
func updateHistory(index: Int, day: Date, uses: String, account: String) {
|
||||
guard let histories = self.histories?.sorted(by: {
|
||||
$0.createAt.compare($1.createAt) == .orderedAscending
|
||||
}) else { return }
|
||||
let history = histories[index]
|
||||
history.update(day: day)
|
||||
history.update(uses: uses)
|
||||
history.update(accounts: account)
|
||||
}
|
||||
|
||||
func appendHistory(history: History) {
|
||||
self.mutableSetValue(forKeyPath: #keyPath(Tag.histories)).add(history)
|
||||
}
|
||||
|
||||
func update(url: String) {
|
||||
if self.url != url {
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
[NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
static func predicate(name: String) -> NSPredicate {
|
||||
NSPredicate(format: "%K == %@", #keyPath(Tag.name), name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -294,7 +294,7 @@
|
|||
"cancel": "Cancel"
|
||||
},
|
||||
"recommend": {
|
||||
"buttonText": "See All",
|
||||
"button_text": "See All",
|
||||
"hash_tag": {
|
||||
"title": "Trending in your timeline",
|
||||
"description": "Hashtags that are getting quite a bit of attention among people you follow",
|
||||
|
@ -305,6 +305,15 @@
|
|||
"description": "Except for Sam, you will not like his account.",
|
||||
"follow": "Follow"
|
||||
}
|
||||
},
|
||||
"searching": {
|
||||
"segment": {
|
||||
"all": "All",
|
||||
"people": "People",
|
||||
"hashtags": "Hashtags"
|
||||
},
|
||||
"recent_search": "Recent searches",
|
||||
"clear": "clear"
|
||||
}
|
||||
},
|
||||
"hashtag": {
|
||||
|
@ -314,4 +323,4 @@
|
|||
"title": "Your Favorites"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,13 @@
|
|||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; };
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */; };
|
||||
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */; };
|
||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
||||
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
|
||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||
|
@ -40,7 +45,7 @@
|
|||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
|
||||
2D34D9CB261489930081BFC0 /* SearchViewController+RecomendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */; };
|
||||
2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */; };
|
||||
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; };
|
||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; };
|
||||
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; };
|
||||
|
@ -86,6 +91,7 @@
|
|||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; };
|
||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
|
||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
|
||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */; };
|
||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
|
||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; };
|
||||
|
@ -105,7 +111,7 @@
|
|||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; };
|
||||
2DE0FAC12615F04D00CDF649 /* RecomendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */; };
|
||||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; };
|
||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */; };
|
||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */; };
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
||||
|
@ -114,6 +120,8 @@
|
|||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; };
|
||||
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
|
||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; };
|
||||
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */; };
|
||||
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; };
|
||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
|
||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
||||
|
@ -127,6 +135,7 @@
|
|||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; };
|
||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */; };
|
||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; };
|
||||
5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; };
|
||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||
|
@ -400,8 +409,13 @@
|
|||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||
2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBottomLoader.swift; sourceTree = "<group>"; };
|
||||
2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
||||
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||
|
@ -410,7 +424,7 @@
|
|||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
||||
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||
2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+RecomendView.swift"; sourceTree = "<group>"; };
|
||||
2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Recommend.swift"; sourceTree = "<group>"; };
|
||||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
|
||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
|
||||
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -453,6 +467,7 @@
|
|||
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
|
||||
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = "<group>"; };
|
||||
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
|
||||
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -472,7 +487,7 @@
|
|||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
||||
2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecomendHashTagSection.swift; sourceTree = "<group>"; };
|
||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = "<group>"; };
|
||||
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = "<group>"; };
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||
|
@ -481,6 +496,8 @@
|
|||
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = "<group>"; };
|
||||
2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = "<group>"; };
|
||||
2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = "<group>"; };
|
||||
2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = "<group>"; };
|
||||
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = "<group>"; };
|
||||
2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = "<group>"; };
|
||||
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -497,6 +514,7 @@
|
|||
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = "<group>"; };
|
||||
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = "<group>"; };
|
||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
||||
5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = "<group>"; };
|
||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -1012,8 +1030,9 @@
|
|||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */,
|
||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||
);
|
||||
|
@ -1063,6 +1082,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7631B225C159F700929FB9 /* Item.swift */,
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
|
@ -1097,6 +1117,15 @@
|
|||
path = Stack;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2DFAD5212616F8E300F9EE7C /* TableViewCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */,
|
||||
2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */,
|
||||
);
|
||||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3FE14AD363ED19AE7FF210A6 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1301,6 +1330,7 @@
|
|||
2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.swift */,
|
||||
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
|
||||
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
|
||||
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1457,6 +1487,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB89BA2625C110B4008580ED /* Status.swift */,
|
||||
2D0B7A1C261D839600B44727 /* SearchHistory.swift */,
|
||||
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
|
||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
|
||||
2D927F0125C7E4F2004F19B8 /* Mention.swift */,
|
||||
|
@ -1594,10 +1625,14 @@
|
|||
DB9D6BEE25E4F5370051B173 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DFAD5212616F8E300F9EE7C /* TableViewCell */,
|
||||
2DE0FAC62615F5D200CDF649 /* View */,
|
||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */,
|
||||
2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */,
|
||||
2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */,
|
||||
2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */,
|
||||
5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */,
|
||||
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */,
|
||||
2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */,
|
||||
2D34D9E026149C550081BFC0 /* CollectionViewCell */,
|
||||
);
|
||||
path = Search;
|
||||
|
@ -2148,6 +2183,7 @@
|
|||
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
|
||||
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */,
|
||||
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
|
||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||
|
@ -2179,6 +2215,7 @@
|
|||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
||||
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */,
|
||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
|
||||
|
@ -2192,6 +2229,7 @@
|
|||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||
2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */,
|
||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
|
@ -2202,6 +2240,7 @@
|
|||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||
|
@ -2232,13 +2271,14 @@
|
|||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
||||
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
|
||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
||||
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
|
||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||
2DE0FAC12615F04D00CDF649 /* RecomendHashTagSection.swift in Sources */,
|
||||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
|
||||
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
|
||||
|
@ -2309,7 +2349,7 @@
|
|||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||
2D34D9CB261489930081BFC0 /* SearchViewController+RecomendView.swift in Sources */,
|
||||
2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */,
|
||||
DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */,
|
||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
|
||||
|
@ -2353,9 +2393,11 @@
|
|||
2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */,
|
||||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
|
||||
5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */,
|
||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */,
|
||||
0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */,
|
||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||
|
@ -2371,6 +2413,7 @@
|
|||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||
|
@ -2435,6 +2478,7 @@
|
|||
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */,
|
||||
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */,
|
||||
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
||||
2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */,
|
||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// SearchResultItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/6.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
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 bottomLoader
|
||||
}
|
||||
|
||||
extension SearchResultItem: Equatable {
|
||||
static func == (lhs: SearchResultItem, rhs: SearchResultItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.hashtag(let tagLeft), .hashtag(let tagRight)):
|
||||
return tagLeft == tagRight
|
||||
case (.account(let accountLeft), .account(let accountRight)):
|
||||
return accountLeft == accountRight
|
||||
case (.bottomLoader, .bottomLoader):
|
||||
return true
|
||||
case (.accountObjectID(let idLeft),.accountObjectID(let idRight)):
|
||||
return idLeft == idRight
|
||||
case (.hashtagObjectID(let idLeft),.hashtagObjectID(let idRight)):
|
||||
return idLeft == idRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .account(let account):
|
||||
hasher.combine(account)
|
||||
case .hashtag(let tag):
|
||||
hasher.combine(tag)
|
||||
case .accountObjectID(let id):
|
||||
hasher.combine(id)
|
||||
case .hashtagObjectID(let id):
|
||||
hasher.combine(id)
|
||||
case .bottomLoader:
|
||||
hasher.combine(String(describing: SearchResultItem.bottomLoader.self))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
// Created by sxiaojian on 2021/4/1.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
|
@ -15,11 +17,15 @@ enum RecommendAccountSection: Equatable, Hashable {
|
|||
|
||||
extension RecommendAccountSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView
|
||||
) -> UICollectionViewDiffableDataSource<RecommendAccountSection, Mastodon.Entity.Account> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, account -> UICollectionViewCell? in
|
||||
for collectionView: UICollectionView,
|
||||
delegate: SearchRecommendAccountsCollectionViewCellDelegate,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
|
||||
cell.config(with: account)
|
||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
cell.delegate = delegate
|
||||
cell.config(with: user)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// RecomendHashTagSection.swift
|
||||
// RecommendHashTagSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/1.
|
||||
|
@ -9,14 +9,14 @@ import Foundation
|
|||
import MastodonSDK
|
||||
import UIKit
|
||||
|
||||
enum RecomendHashTagSection: Equatable, Hashable {
|
||||
enum RecommendHashTagSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension RecomendHashTagSection {
|
||||
extension RecommendHashTagSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView
|
||||
) -> UICollectionViewDiffableDataSource<RecomendHashTagSection, Mastodon.Entity.Tag> {
|
||||
) -> UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, tag -> UICollectionViewCell? in
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self), for: indexPath) as! SearchRecommendTagsCollectionViewCell
|
||||
cell.config(with: tag)
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// SearchResultSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
enum SearchResultSection: Equatable, Hashable {
|
||||
case account
|
||||
case hashtag
|
||||
case mixed
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
extension SearchResultSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency
|
||||
) -> UITableViewDiffableDataSource<SearchResultSection, SearchResultItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in
|
||||
switch result {
|
||||
case .account(let account):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
||||
cell.config(with: account)
|
||||
return cell
|
||||
case .hashtag(let tag):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
||||
cell.config(with: tag)
|
||||
return cell
|
||||
case .hashtagObjectID(let hashtagObjectID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
||||
let tag = dependency.context.managedObjectContext.object(with: hashtagObjectID) as! Tag
|
||||
cell.config(with: tag)
|
||||
return cell
|
||||
case .accountObjectID(let accountObjectID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell
|
||||
let user = dependency.context.managedObjectContext.object(with: accountObjectID) as! MastodonUser
|
||||
cell.config(with: user)
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader
|
||||
cell.startAnimating()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,8 +42,9 @@ internal enum Asset {
|
|||
internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border")
|
||||
internal static let danger = ColorAsset(name: "Colors/Background/danger")
|
||||
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
|
||||
internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar")
|
||||
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
||||
internal static let search = ColorAsset(name: "Colors/Background/search")
|
||||
internal static let searchResult = ColorAsset(name: "Colors/Background/searchResult")
|
||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||
|
@ -51,6 +52,9 @@ internal enum Asset {
|
|||
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
||||
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
||||
}
|
||||
internal enum Border {
|
||||
internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard")
|
||||
}
|
||||
internal enum Button {
|
||||
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
||||
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
|
||||
|
@ -66,6 +70,9 @@ internal enum Asset {
|
|||
internal static let primary = ColorAsset(name: "Colors/Label/primary")
|
||||
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
|
||||
}
|
||||
internal enum Shadow {
|
||||
internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard")
|
||||
}
|
||||
internal enum Slider {
|
||||
internal static let bar = ColorAsset(name: "Colors/Slider/bar")
|
||||
}
|
||||
|
|
|
@ -488,7 +488,7 @@ internal enum L10n {
|
|||
internal enum Search {
|
||||
internal enum Recommend {
|
||||
/// See All
|
||||
internal static let buttontext = L10n.tr("Localizable", "Scene.Search.Recommend.Buttontext")
|
||||
internal static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText")
|
||||
internal enum Accounts {
|
||||
/// Except for Sam, you will not like his account.
|
||||
internal static let description = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Description")
|
||||
|
@ -514,6 +514,20 @@ internal enum L10n {
|
|||
/// Search hashtags and users
|
||||
internal static let placeholder = L10n.tr("Localizable", "Scene.Search.Searchbar.Placeholder")
|
||||
}
|
||||
internal enum Searching {
|
||||
/// clear
|
||||
internal static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear")
|
||||
/// Recent searches
|
||||
internal static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch")
|
||||
internal enum Segment {
|
||||
/// All
|
||||
internal static let all = L10n.tr("Localizable", "Scene.Search.Searching.Segment.All")
|
||||
/// Hashtags
|
||||
internal static let hashtags = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Hashtags")
|
||||
/// People
|
||||
internal static let people = L10n.tr("Localizable", "Scene.Search.Searching.Segment.People")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum ServerPicker {
|
||||
/// Pick a Server,\nany server.
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.940",
|
||||
"blue" : "249",
|
||||
"green" : "249",
|
||||
"red" : "249"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.940",
|
||||
"blue" : "29",
|
||||
"green" : "29",
|
||||
"red" : "29"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "232",
|
||||
"green" : "225",
|
||||
"red" : "217"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFE",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.300",
|
||||
"blue" : "213",
|
||||
"green" : "213",
|
||||
"red" : "213"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0",
|
||||
"green" : "0",
|
||||
"red" : "0"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -158,12 +158,17 @@ tap the link to confirm your account.";
|
|||
"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account.";
|
||||
"Scene.Search.Recommend.Accounts.Follow" = "Follow";
|
||||
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
|
||||
"Scene.Search.Recommend.Buttontext" = "See All";
|
||||
"Scene.Search.Recommend.ButtonText" = "See All";
|
||||
"Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention among people you follow";
|
||||
"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking";
|
||||
"Scene.Search.Recommend.HashTag.Title" = "Trending in your timeline";
|
||||
"Scene.Search.Searchbar.Cancel" = "Cancel";
|
||||
"Scene.Search.Searchbar.Placeholder" = "Search hashtags and users";
|
||||
"Scene.Search.Searching.Clear" = "clear";
|
||||
"Scene.Search.Searching.RecentSearch" = "Recent searches";
|
||||
"Scene.Search.Searching.Segment.All" = "All";
|
||||
"Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
|
||||
"Scene.Search.Searching.Segment.People" = "People";
|
||||
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||
"Scene.ServerPicker.Button.SeeLess" = "See Less";
|
||||
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||
|
|
|
@ -5,28 +5,47 @@
|
|||
// Created by sxiaojian on 2021/4/1.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
|
||||
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
|
||||
func followButtonDidPressed(clickedUser: MastodonUser)
|
||||
|
||||
func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton)
|
||||
}
|
||||
|
||||
class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate?
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.layer.cornerRadius = 8
|
||||
imageView.layer.cornerRadius = 8.4
|
||||
imageView.clipsToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let headerImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.layer.cornerRadius = 8
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = 10
|
||||
imageView.layer.cornerCurve = .continuous
|
||||
imageView.clipsToBounds = true
|
||||
imageView.layer.borderWidth = 2
|
||||
imageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
||||
|
||||
let displayNameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
|
@ -36,17 +55,19 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
|||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
label.textAlignment = .center
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let followButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
let followButton: HighlightDimmableButton = {
|
||||
let button = HighlightDimmableButton(type: .custom)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||
button.layer.cornerRadius = 12
|
||||
button.layer.borderWidth = 3
|
||||
button.layer.cornerCurve = .continuous
|
||||
button.layer.borderWidth = 2
|
||||
button.layer.borderColor = UIColor.white.cgColor
|
||||
return button
|
||||
}()
|
||||
|
@ -55,6 +76,8 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
|||
super.prepareForReuse()
|
||||
headerImageView.af.cancelImageRequest()
|
||||
avatarImageView.af.cancelImageRequest()
|
||||
visualEffectView.removeFromSuperview()
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -69,11 +92,18 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
|||
}
|
||||
|
||||
extension SearchRecommendAccountsCollectionViewCell {
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
headerImageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
headerImageView.backgroundColor = Asset.Colors.brandBlue.color
|
||||
layer.cornerRadius = 8
|
||||
clipsToBounds = true
|
||||
|
||||
layer.cornerRadius = 10
|
||||
layer.cornerCurve = .continuous
|
||||
clipsToBounds = false
|
||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||
contentView.addSubview(headerImageView)
|
||||
headerImageView.pin(top: 16, left: 0, bottom: 0, right: 0)
|
||||
|
||||
|
@ -87,12 +117,16 @@ extension SearchRecommendAccountsCollectionViewCell {
|
|||
contentView.addSubview(displayNameLabel)
|
||||
displayNameLabel.constrain([
|
||||
displayNameLabel.constraint(.top, toView: contentView, constant: 108),
|
||||
displayNameLabel.constraint(.leading, toView: contentView),
|
||||
displayNameLabel.constraint(.trailing, toView: contentView),
|
||||
displayNameLabel.constraint(.centerX, toView: contentView)
|
||||
])
|
||||
|
||||
contentView.addSubview(acctLabel)
|
||||
acctLabel.constrain([
|
||||
acctLabel.constraint(.top, toView: contentView, constant: 132),
|
||||
acctLabel.constraint(.leading, toView: contentView),
|
||||
acctLabel.constraint(.trailing, toView: contentView),
|
||||
acctLabel.constraint(.centerX, toView: contentView)
|
||||
])
|
||||
|
||||
|
@ -104,19 +138,33 @@ extension SearchRecommendAccountsCollectionViewCell {
|
|||
])
|
||||
}
|
||||
|
||||
func config(with account: Mastodon.Entity.Account) {
|
||||
displayNameLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||
acctLabel.text = account.acct
|
||||
func config(with mastodonUser: MastodonUser) {
|
||||
displayNameLabel.text = mastodonUser.displayName.isEmpty ? mastodonUser.username : mastodonUser.displayName
|
||||
acctLabel.text = mastodonUser.acct
|
||||
avatarImageView.af.setImage(
|
||||
withURL: URL(string: account.avatar)!,
|
||||
withURL: URL(string: mastodonUser.avatar)!,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
headerImageView.af.setImage(
|
||||
withURL: URL(string: account.header)!,
|
||||
withURL: URL(string: mastodonUser.header)!,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.headerImageView.addSubview(self.visualEffectView)
|
||||
self.visualEffectView.pin(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
delegate?.configFollowButton(with: mastodonUser, followButton: followButton)
|
||||
followButton.publisher(for: .touchUpInside)
|
||||
.sink { [weak self] _ in
|
||||
self?.followButtonDidPressed(mastodonUser: mastodonUser)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func followButtonDidPressed(mastodonUser: MastodonUser) {
|
||||
delegate?.followButtonDidPressed(clickedUser: mastodonUser)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
|||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let hashTagTitleLabel: UILabel = {
|
||||
|
||||
let hashtagTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
|
@ -58,16 +58,27 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
|
|||
}
|
||||
|
||||
extension SearchRecommendTagsCollectionViewCell {
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
backgroundColor = Asset.Colors.brandBlue.color
|
||||
layer.cornerRadius = 8
|
||||
clipsToBounds = true
|
||||
layer.cornerRadius = 10
|
||||
layer.cornerCurve = .continuous
|
||||
clipsToBounds = false
|
||||
layer.borderWidth = 2
|
||||
layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor
|
||||
applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0)
|
||||
|
||||
contentView.addSubview(backgroundImageView)
|
||||
backgroundImageView.constrain(toSuperviewEdges: nil)
|
||||
|
||||
contentView.addSubview(hashTagTitleLabel)
|
||||
hashTagTitleLabel.pin(top: 16, left: 16, bottom: nil, right: 42)
|
||||
contentView.addSubview(hashtagTitleLabel)
|
||||
hashtagTitleLabel.pin(top: 16, left: 16, bottom: nil, right: 42)
|
||||
|
||||
contentView.addSubview(peopleLabel)
|
||||
peopleLabel.pinTopLeft(top: 46, left: 16)
|
||||
|
@ -77,19 +88,13 @@ extension SearchRecommendTagsCollectionViewCell {
|
|||
}
|
||||
|
||||
func config(with tag: Mastodon.Entity.Tag) {
|
||||
hashTagTitleLabel.text = "# " + tag.name
|
||||
hashtagTitleLabel.text = "# " + tag.name
|
||||
guard let historys = tag.history else {
|
||||
peopleLabel.text = ""
|
||||
return
|
||||
}
|
||||
var recentHistory = [Mastodon.Entity.History]()
|
||||
for history in historys {
|
||||
if Int(history.uses) == 0 {
|
||||
break
|
||||
} else {
|
||||
recentHistory.append(history)
|
||||
}
|
||||
}
|
||||
|
||||
let recentHistory = historys.prefix(2)
|
||||
let peopleAreTalking = recentHistory.compactMap({ Int($0.accounts) }).reduce(0, +)
|
||||
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
||||
peopleLabel.text = string
|
||||
|
@ -105,7 +110,7 @@ struct SearchRecommendTagsCollectionViewCell_Previews: PreviewProvider {
|
|||
Group {
|
||||
UIViewPreview {
|
||||
let cell = SearchRecommendTagsCollectionViewCell()
|
||||
cell.hashTagTitleLabel.text = "# test"
|
||||
cell.hashtagTitleLabel.text = "# test"
|
||||
cell.peopleLabel.text = "128 people are talking"
|
||||
return cell
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
//
|
||||
// SearchViewController+Follow.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/4/9.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension SearchViewController: UserProvider {
|
||||
func mastodonUser() -> Future<MastodonUser?, Never> {
|
||||
Future { promise in
|
||||
promise(.success(self.viewModel.mastodonUser.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate {
|
||||
func followButtonDidPressed(clickedUser: MastodonUser) {
|
||||
viewModel.mastodonUser.value = clickedUser
|
||||
guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
|
||||
return
|
||||
}
|
||||
guard let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) else { return }
|
||||
switch relationshipAction {
|
||||
case .none:
|
||||
break
|
||||
case .follow, .following:
|
||||
UserProviderFacade.toggleUserFollowRelationship(provider: self)
|
||||
.sink { _ in
|
||||
|
||||
} receiveValue: { _ in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
case .pending:
|
||||
break
|
||||
case .muting:
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
let name = mastodonUser.displayNameWithFallback
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unmute, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
UserProviderFacade.toggleUserMuteRelationship(provider: self)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.context.disposeBag)
|
||||
}
|
||||
alertController.addAction(unmuteAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
case .blocking:
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
let name = mastodonUser.displayNameWithFallback
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unblock, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
UserProviderFacade.toggleUserBlockRelationship(provider: self)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.context.disposeBag)
|
||||
}
|
||||
alertController.addAction(unblockAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
case .blocked:
|
||||
break
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) {
|
||||
guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
|
||||
return
|
||||
}
|
||||
_configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser, followButton: followButton)
|
||||
ManagedObjectObserver.observe(object: currentMastodonUser)
|
||||
.sink { _ in
|
||||
|
||||
} receiveValue: { change in
|
||||
guard case .update(let object) = change.changeType,
|
||||
let newUser = object as? MastodonUser else { return }
|
||||
self._configFollowButton(with: mastodonUser, currentMastodonUser: newUser, followButton: followButton)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewController {
|
||||
func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser, followButton: HighlightDimmableButton) {
|
||||
let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||
followButton.setTitle(relationshipActionSet.title, for: .normal)
|
||||
}
|
||||
|
||||
func relationShipActionSet(mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) -> ProfileViewModel.RelationshipActionOptionSet {
|
||||
var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
|
||||
let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isFollowing {
|
||||
relationshipActionSet.insert(.following)
|
||||
}
|
||||
|
||||
let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isPending {
|
||||
relationshipActionSet.insert(.pending)
|
||||
}
|
||||
|
||||
let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isBlocking {
|
||||
relationshipActionSet.insert(.blocking)
|
||||
}
|
||||
|
||||
let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
|
||||
if isBlockedBy {
|
||||
relationshipActionSet.insert(.blocked)
|
||||
}
|
||||
return relationshipActionSet
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
//
|
||||
// SearchViewController+RecomendView.swift
|
||||
// SearchViewController+Recommend.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/3/31.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import OSLog
|
||||
|
@ -15,32 +17,16 @@ extension SearchViewController {
|
|||
let header = SearchRecommendCollectionHeader()
|
||||
header.titleLabel.text = L10n.Scene.Search.Recommend.HashTag.title
|
||||
header.descriptionLabel.text = L10n.Scene.Search.Recommend.HashTag.description
|
||||
header.seeAllButton.addTarget(self, action: #selector(SearchViewController.hashTagSeeAllButtonPressed(_:)), for: .touchUpInside)
|
||||
header.seeAllButton.addTarget(self, action: #selector(SearchViewController.hashtagSeeAllButtonPressed(_:)), for: .touchUpInside)
|
||||
stackView.addArrangedSubview(header)
|
||||
|
||||
hashTagCollectionView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self))
|
||||
hashTagCollectionView.delegate = self
|
||||
hashtagCollectionView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self))
|
||||
hashtagCollectionView.delegate = self
|
||||
|
||||
stackView.addArrangedSubview(hashTagCollectionView)
|
||||
hashTagCollectionView.constrain([
|
||||
hashTagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130)
|
||||
stackView.addArrangedSubview(hashtagCollectionView)
|
||||
hashtagCollectionView.constrain([
|
||||
hashtagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130)
|
||||
])
|
||||
|
||||
viewModel.requestRecommendHashTags()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if !self.viewModel.recommendHashTags.isEmpty {
|
||||
let dataSource = RecomendHashTagSection.collectionViewDiffableDataSource(for: self.hashTagCollectionView)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<RecomendHashTagSection, Mastodon.Entity.Tag>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.viewModel.recommendHashTags, toSection: .main)
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
self.hashTagDiffableDataSource = dataSource
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func setupAccountsCollectionView() {
|
||||
|
@ -57,27 +43,11 @@ extension SearchViewController {
|
|||
accountsCollectionView.constrain([
|
||||
accountsCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 202)
|
||||
])
|
||||
|
||||
viewModel.requestRecommendAccounts()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if !self.viewModel.recommendAccounts.isEmpty {
|
||||
let dataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: self.accountsCollectionView)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, Mastodon.Entity.Account>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.viewModel.recommendAccounts, toSection: .main)
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
self.accountDiffableDataSource = dataSource
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
hashTagCollectionView.collectionViewLayout.invalidateLayout()
|
||||
hashtagCollectionView.collectionViewLayout.invalidateLayout()
|
||||
accountsCollectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +56,19 @@ extension SearchViewController: UICollectionViewDelegate {
|
|||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", (#file as NSString).lastPathComponent, #line, #function, indexPath.debugDescription)
|
||||
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
switch collectionView {
|
||||
case self.accountsCollectionView:
|
||||
guard let diffableDataSource = viewModel.accountDiffableDataSource else { return }
|
||||
guard let accountObjectID = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
let user = context.managedObjectContext.object(with: accountObjectID) as! MastodonUser
|
||||
viewModel.accountCollectionViewItemDidSelected(mastodonUser: user, from: self)
|
||||
case self.hashtagCollectionView:
|
||||
guard let diffableDataSource = viewModel.hashtagDiffableDataSource else { return }
|
||||
guard let hashtag = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
viewModel.hashtagCollectionViewItemDidSelected(hashtag: hashtag, from: self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +80,7 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout {
|
|||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
if collectionView == hashTagCollectionView {
|
||||
if collectionView == hashtagCollectionView {
|
||||
return 6
|
||||
} else {
|
||||
return 12
|
||||
|
@ -105,7 +88,7 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout {
|
|||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
if collectionView == hashTagCollectionView {
|
||||
if collectionView == hashtagCollectionView {
|
||||
return CGSize(width: 228, height: 130)
|
||||
} else {
|
||||
return CGSize(width: 257, height: 202)
|
||||
|
@ -114,7 +97,7 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout {
|
|||
}
|
||||
|
||||
extension SearchViewController {
|
||||
@objc func hashTagSeeAllButtonPressed(_ sender: UIButton) {}
|
||||
@objc func hashtagSeeAllButtonPressed(_ sender: UIButton) {}
|
||||
|
||||
@objc func accountSeeAllButtonPressed(_ sender: UIButton) {}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// SearchViewController+Searching.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/2.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import OSLog
|
||||
import UIKit
|
||||
|
||||
extension SearchViewController {
|
||||
func setupSearchingTableView() {
|
||||
searchingTableView.delegate = self
|
||||
searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self))
|
||||
searchingTableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self))
|
||||
view.addSubview(searchingTableView)
|
||||
searchingTableView.constrain([
|
||||
searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||
searchingTableView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
searchingTableView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
searchingTableView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor)
|
||||
])
|
||||
searchingTableView.tableFooterView = UIView()
|
||||
viewModel.isSearching
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isSearching in
|
||||
self?.searchingTableView.isHidden = !isSearching
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
viewModel.isSearching,
|
||||
viewModel.searchText
|
||||
)
|
||||
.sink { [weak self] isSearching, text in
|
||||
guard let self = self else { return }
|
||||
if isSearching, text.isEmpty {
|
||||
self.searchingTableView.tableHeaderView = self.searchHeader
|
||||
} else {
|
||||
self.searchingTableView.tableHeaderView = nil
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func setupSearchHeader() {
|
||||
searchHeader.addSubview(recentSearchesLabel)
|
||||
recentSearchesLabel.constrain([
|
||||
recentSearchesLabel.constraint(.leading, toView: searchHeader, constant: 16),
|
||||
recentSearchesLabel.constraint(.centerY, toView: searchHeader)
|
||||
])
|
||||
|
||||
searchHeader.addSubview(clearSearchHistoryButton)
|
||||
recentSearchesLabel.constrain([
|
||||
searchHeader.trailingAnchor.constraint(equalTo: clearSearchHistoryButton.trailingAnchor, constant: 16),
|
||||
clearSearchHistoryButton.constraint(.centerY, toView: searchHeader)
|
||||
])
|
||||
|
||||
clearSearchHistoryButton.addTarget(self, action: #selector(SearchViewController.clearAction(_:)), for: .touchUpInside)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewController {
|
||||
@objc func clearAction(_ sender: UIButton) {
|
||||
viewModel.deleteSearchHistory()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
extension SearchViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
66
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
66
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
viewModel.searchResultItemDidSelected(item: item, from: self)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Combine
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
|
||||
|
@ -14,7 +15,13 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private(set) lazy var viewModel = SearchViewModel(context: context)
|
||||
private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator)
|
||||
|
||||
let statusBar: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.navigationBar.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let searchBar: UISearchBar = {
|
||||
let searchBar = UISearchBar()
|
||||
|
@ -24,9 +31,12 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
let micImage = UIImage(systemName: "mic.fill")
|
||||
searchBar.setImage(micImage, for: .bookmark, state: .normal)
|
||||
searchBar.showsBookmarkButton = true
|
||||
searchBar.scopeButtonTitles = [L10n.Scene.Search.Searching.Segment.all, L10n.Scene.Search.Searching.Segment.people, L10n.Scene.Search.Searching.Segment.hashtags]
|
||||
searchBar.barTintColor = Asset.Colors.Background.navigationBar.color
|
||||
return searchBar
|
||||
}()
|
||||
|
||||
// recommend
|
||||
let scrollView: UIScrollView = {
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
|
@ -45,7 +55,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
return stackView
|
||||
}()
|
||||
|
||||
let hashTagCollectionView: UICollectionView = {
|
||||
let hashtagCollectionView: UICollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.scrollDirection = .horizontal
|
||||
let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
|
@ -56,9 +66,6 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
var hashTagDiffableDataSource: UICollectionViewDiffableDataSource<RecomendHashTagSection, Mastodon.Entity.Tag>?
|
||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, Mastodon.Entity.Account>?
|
||||
|
||||
let accountsCollectionView: UICollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
|
@ -71,24 +78,82 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
// searching
|
||||
let searchingTableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.backgroundColor = Asset.Colors.Background.searchResult.color
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .singleLine
|
||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
lazy var searchHeader: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
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
|
||||
}()
|
||||
}
|
||||
|
||||
extension SearchViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Asset.Colors.Background.search.color
|
||||
searchBar.delegate = self
|
||||
navigationItem.titleView = searchBar
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
navigationItem.hidesBackButton = true
|
||||
|
||||
setupSearchBar()
|
||||
setupScrollView()
|
||||
setupHashTagCollectionView()
|
||||
setupAccountsCollectionView()
|
||||
setupSearchingTableView()
|
||||
setupDataSource()
|
||||
setupSearchHeader()
|
||||
}
|
||||
|
||||
func setupSearchBar() {
|
||||
searchBar.delegate = self
|
||||
view.addSubview(searchBar)
|
||||
searchBar.constrain([
|
||||
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
view.addSubview(statusBar)
|
||||
|
||||
statusBar.constrain([
|
||||
statusBar.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
statusBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 3),
|
||||
])
|
||||
}
|
||||
|
||||
func setupScrollView() {
|
||||
view.addSubview(scrollView)
|
||||
scrollView.constrain([
|
||||
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
|
@ -104,30 +169,70 @@ extension SearchViewController {
|
|||
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func setupDataSource() {
|
||||
viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView)
|
||||
viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, delegate: self, managedObjectContext: context.managedObjectContext)
|
||||
viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewController: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if scrollView == searchingTableView {
|
||||
handleScrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewController: UISearchBarDelegate {
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(true, animated: true)
|
||||
searchBar.showsScopeBar = true
|
||||
viewModel.isSearching.value = true
|
||||
}
|
||||
|
||||
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(false, animated: true)
|
||||
searchBar.showsScopeBar = false
|
||||
viewModel.isSearching.value = true
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(false, animated: true)
|
||||
searchBar.showsScopeBar = false
|
||||
searchBar.text = ""
|
||||
searchBar.resignFirstResponder()
|
||||
viewModel.isSearching.value = false
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
viewModel.searchText.send(searchText)
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||
switch selectedScope {
|
||||
case 0:
|
||||
viewModel.searchScope.value = Mastodon.API.Search.SearchType.default
|
||||
case 1:
|
||||
viewModel.searchScope.value = Mastodon.API.Search.SearchType.accounts
|
||||
case 2:
|
||||
viewModel.searchScope.value = Mastodon.API.Search.SearchType.hashtags
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {}
|
||||
}
|
||||
|
||||
extension SearchViewController: LoadMoreConfigurableTableViewContainer {
|
||||
typealias BottomLoaderTableViewCell = SearchBottomLoader
|
||||
typealias LoadingState = SearchViewModel.LoadOldestState.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { searchingTableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
//
|
||||
// SearchViewModel+LoadOldestState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
|
||||
extension SearchViewModel {
|
||||
class LoadOldestState: GKState {
|
||||
weak var viewModel: SearchViewModel?
|
||||
|
||||
init(viewModel: SearchViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, debugDescription, previousState.debugDescription)
|
||||
viewModel?.loadOldestStateMachinePublisher.send(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewModel.LoadOldestState {
|
||||
class Initial: SearchViewModel.LoadOldestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard viewModel.searchResult.value != nil else { return false }
|
||||
return stateClass == Loading.self
|
||||
}
|
||||
}
|
||||
|
||||
class Loading: SearchViewModel.LoadOldestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
assertionFailure()
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
guard let oldSearchResult = viewModel.searchResult.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
var offset = 0
|
||||
switch viewModel.searchScope.value {
|
||||
case Mastodon.API.Search.SearchType.accounts:
|
||||
offset = oldSearchResult.accounts.count
|
||||
case Mastodon.API.Search.SearchType.hashtags:
|
||||
offset = oldSearchResult.hashtags.count
|
||||
default:
|
||||
return
|
||||
}
|
||||
let query = Mastodon.API.Search.Query(q: viewModel.searchText.value,
|
||||
type: viewModel.searchScope.value,
|
||||
accountID: nil,
|
||||
maxID: nil,
|
||||
minID: nil,
|
||||
excludeUnreviewed: nil,
|
||||
resolve: nil,
|
||||
limit: nil,
|
||||
offset: offset,
|
||||
following: nil)
|
||||
|
||||
viewModel.context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log("%{public}s[%{public}ld], %{public}s: load oldest search failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||
case .finished:
|
||||
// handle isFetchingLatestTimeline in fetch controller delegate
|
||||
break
|
||||
}
|
||||
} receiveValue: { result in
|
||||
switch viewModel.searchScope.value {
|
||||
case Mastodon.API.Search.SearchType.accounts:
|
||||
if result.value.accounts.isEmpty {
|
||||
stateMachine.enter(NoMore.self)
|
||||
} else {
|
||||
var newAccounts = [Mastodon.Entity.Account]()
|
||||
newAccounts.append(contentsOf: oldSearchResult.accounts)
|
||||
newAccounts.append(contentsOf: result.value.accounts)
|
||||
newAccounts.removeDuplicates()
|
||||
viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: newAccounts, statuses: oldSearchResult.statuses, hashtags: oldSearchResult.hashtags)
|
||||
stateMachine.enter(Idle.self)
|
||||
}
|
||||
case Mastodon.API.Search.SearchType.hashtags:
|
||||
if result.value.hashtags.isEmpty {
|
||||
stateMachine.enter(NoMore.self)
|
||||
} else {
|
||||
var newTags = [Mastodon.Entity.Tag]()
|
||||
newTags.append(contentsOf: oldSearchResult.hashtags)
|
||||
newTags.append(contentsOf: result.value.hashtags)
|
||||
newTags.removeDuplicates()
|
||||
viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: oldSearchResult.accounts, statuses: oldSearchResult.statuses, hashtags: newTags)
|
||||
stateMachine.enter(Idle.self)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
class Fail: SearchViewModel.LoadOldestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
stateClass == Loading.self || stateClass == Idle.self
|
||||
}
|
||||
}
|
||||
|
||||
class Idle: SearchViewModel.LoadOldestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
stateClass == Loading.self
|
||||
}
|
||||
}
|
||||
|
||||
class NoMore: SearchViewModel.LoadOldestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// reset state if needs
|
||||
stateClass == Idle.self
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
guard let viewModel = viewModel else { return }
|
||||
guard let diffableDataSource = viewModel.searchResultDiffableDataSource else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.deleteItems([.bottomLoader])
|
||||
diffableDataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,25 +6,201 @@
|
|||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import OSLog
|
||||
import UIKit
|
||||
|
||||
final class SearchViewModel {
|
||||
final class SearchViewModel: NSObject {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
let mastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
||||
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
||||
|
||||
// output
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
let searchScope = CurrentValueSubject<Mastodon.API.Search.SearchType, Never>(Mastodon.API.Search.SearchType.default)
|
||||
|
||||
let isSearching = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
let searchResult = CurrentValueSubject<Mastodon.Entity.SearchResult?, Never>(nil)
|
||||
|
||||
var recommendHashTags = [Mastodon.Entity.Tag]()
|
||||
var recommendAccounts = [Mastodon.Entity.Account]()
|
||||
var recommendAccounts = [NSManagedObjectID]()
|
||||
|
||||
init(context: AppContext) {
|
||||
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
|
||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
||||
var searchResultDiffableDataSource: UITableViewDiffableDataSource<SearchResultSection, SearchResultItem>?
|
||||
|
||||
// bottom loader
|
||||
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
LoadOldestState.Initial(viewModel: self),
|
||||
LoadOldestState.Loading(viewModel: self),
|
||||
LoadOldestState.Fail(viewModel: self),
|
||||
LoadOldestState.Idle(viewModel: self),
|
||||
LoadOldestState.NoMore(viewModel: self),
|
||||
])
|
||||
stateMachine.enter(LoadOldestState.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
|
||||
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||
self.coordinator = coordinator
|
||||
self.context = context
|
||||
super.init()
|
||||
|
||||
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
searchText
|
||||
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
|
||||
searchScope
|
||||
)
|
||||
.filter { text, _ in
|
||||
!text.isEmpty
|
||||
}
|
||||
.flatMap { (text, scope) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> in
|
||||
|
||||
let query = Mastodon.API.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)
|
||||
}
|
||||
.sink { _ in
|
||||
} receiveValue: { [weak self] result in
|
||||
self?.searchResult.value = result.value
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
isSearching
|
||||
.sink { [weak self] isSearching in
|
||||
if !isSearching {
|
||||
self?.searchResult.value = nil
|
||||
self?.searchText.value = ""
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
isSearching,
|
||||
searchText,
|
||||
searchScope
|
||||
)
|
||||
.filter { isSearching, _, _ in
|
||||
isSearching
|
||||
}
|
||||
.sink { [weak self] _, text, scope in
|
||||
guard text.isEmpty else { return }
|
||||
guard let self = self else { return }
|
||||
guard let searchHistories = self.fetchSearchHistory() else { return }
|
||||
guard let dataSource = self.searchResultDiffableDataSource else { return }
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchResultSection, SearchResultItem>()
|
||||
snapshot.appendSections([.mixed])
|
||||
|
||||
searchHistories.forEach { searchHistory in
|
||||
let containsAccount = scope == Mastodon.API.Search.SearchType.accounts || scope == Mastodon.API.Search.SearchType.default
|
||||
let containsHashTag = scope == Mastodon.API.Search.SearchType.hashtags || scope == Mastodon.API.Search.SearchType.default
|
||||
if let mastodonUser = searchHistory.account, containsAccount {
|
||||
let item = SearchResultItem.accountObjectID(accountObjectID: mastodonUser.objectID)
|
||||
snapshot.appendItems([item], toSection: .mixed)
|
||||
}
|
||||
if let tag = searchHistory.hashtag, containsHashTag {
|
||||
let item = SearchResultItem.hashtagObjectID(hashtagObjectID: tag.objectID)
|
||||
snapshot.appendItems([item], toSection: .mixed)
|
||||
}
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
requestRecommendHashTags()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if !self.recommendHashTags.isEmpty {
|
||||
guard let dataSource = self.hashtagDiffableDataSource else { return }
|
||||
var snapshot = NSDiffableDataSourceSnapshot<RecommendHashTagSection, Mastodon.Entity.Tag>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.recommendHashTags, toSection: .main)
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
requestRecommendAccounts()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if !self.recommendAccounts.isEmpty {
|
||||
guard let dataSource = self.accountDiffableDataSource else { return }
|
||||
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.recommendAccounts, toSection: .main)
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
searchResult
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] searchResult in
|
||||
guard let self = self else { return }
|
||||
guard let dataSource = self.searchResultDiffableDataSource else { return }
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SearchResultSection, SearchResultItem>()
|
||||
if let accounts = searchResult?.accounts {
|
||||
snapshot.appendSections([.account])
|
||||
let items = accounts.compactMap { SearchResultItem.account(account: $0) }
|
||||
snapshot.appendItems(items, toSection: .account)
|
||||
if self.searchScope.value == Mastodon.API.Search.SearchType.accounts, !items.isEmpty {
|
||||
snapshot.appendItems([.bottomLoader], toSection: .account)
|
||||
}
|
||||
}
|
||||
if let tags = searchResult?.hashtags {
|
||||
snapshot.appendSections([.hashtag])
|
||||
let items = tags.compactMap { SearchResultItem.hashtag(tag: $0) }
|
||||
snapshot.appendItems(items, toSection: .hashtag)
|
||||
if self.searchScope.value == Mastodon.API.Search.SearchType.hashtags, !items.isEmpty {
|
||||
snapshot.appendItems([.bottomLoader], toSection: .hashtag)
|
||||
}
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func requestRecommendHashTags() -> Future<Void, Error> {
|
||||
|
@ -69,9 +245,162 @@ final class SearchViewModel {
|
|||
}
|
||||
} receiveValue: { [weak self] accounts in
|
||||
guard let self = self else { return }
|
||||
self.recommendAccounts = accounts.value
|
||||
let ids = accounts.value.compactMap({$0.id})
|
||||
let userFetchRequest = MastodonUser.sortedFetchRequest
|
||||
userFetchRequest.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids)
|
||||
let mastodonUsers: [MastodonUser]? = {
|
||||
let userFetchRequest = MastodonUser.sortedFetchRequest
|
||||
userFetchRequest.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids)
|
||||
userFetchRequest.returnsObjectsAsFaults = false
|
||||
do {
|
||||
return try self.context.managedObjectContext.fetch(userFetchRequest)
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
if let users = mastodonUsers {
|
||||
self.recommendAccounts = users.map(\.objectID)
|
||||
}
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) {
|
||||
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
|
||||
DispatchQueue.main.async {
|
||||
self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show)
|
||||
}
|
||||
}
|
||||
|
||||
func hashtagCollectionViewItemDidSelected(hashtag: Mastodon.Entity.Tag, from: UIViewController) {
|
||||
let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: context.managedObjectContext, entity: hashtag)
|
||||
let viewModel = HashtagTimelineViewModel(context: context, hashtag: tagInCoreData.name)
|
||||
DispatchQueue.main.async {
|
||||
self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// SearchBottomLoader.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class SearchBottomLoader: UITableViewCell {
|
||||
let activityIndicatorView: UIActivityIndicatorView = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
activityIndicatorView.tintColor = Asset.Colors.Label.primary.color
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
return activityIndicatorView
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
activityIndicatorView.startAnimating()
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
activityIndicatorView.stopAnimating()
|
||||
}
|
||||
|
||||
func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
contentView.addSubview(activityIndicatorView)
|
||||
activityIndicatorView.constrainToCenter()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// SearchingTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/2.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
|
||||
final class SearchingTableViewCell: UITableViewCell {
|
||||
let _imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||
imageView.layer.cornerRadius = 4
|
||||
imageView.clipsToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let _titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
||||
let _subTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
return label
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
_imageView.af.cancelImageRequest()
|
||||
_imageView.image = nil
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchingTableViewCell {
|
||||
private func configure() {
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
contentView.addSubview(_imageView)
|
||||
_imageView.pin(toSize: CGSize(width: 42, height: 42))
|
||||
_imageView.constrain([
|
||||
_imageView.constraint(.leading, toView: contentView, constant: 21),
|
||||
_imageView.constraint(.centerY, toView: contentView)
|
||||
])
|
||||
|
||||
contentView.addSubview(_titleLabel)
|
||||
_titleLabel.pin(top: 12, left: 75, bottom: nil, right: 0)
|
||||
|
||||
contentView.addSubview(_subTitleLabel)
|
||||
_subTitleLabel.pin(top: 34, left: 75, bottom: nil, right: 0)
|
||||
}
|
||||
|
||||
func config(with account: Mastodon.Entity.Account) {
|
||||
_imageView.af.setImage(
|
||||
withURL: URL(string: account.avatar)!,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||
_subTitleLabel.text = account.acct
|
||||
}
|
||||
|
||||
func config(with account: MastodonUser) {
|
||||
_imageView.af.setImage(
|
||||
withURL: URL(string: account.avatar)!,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||
_subTitleLabel.text = account.acct
|
||||
}
|
||||
|
||||
func config(with tag: Mastodon.Entity.Tag) {
|
||||
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
|
||||
_imageView.image = image
|
||||
_titleLabel.text = "# " + tag.name
|
||||
guard let historys = tag.history else {
|
||||
_subTitleLabel.text = ""
|
||||
return
|
||||
}
|
||||
let recentHistory = historys.prefix(2)
|
||||
let peopleAreTalking = recentHistory.compactMap { Int($0.accounts) }.reduce(0, +)
|
||||
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
||||
_subTitleLabel.text = string
|
||||
}
|
||||
|
||||
func config(with tag: Tag) {
|
||||
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
|
||||
_imageView.image = image
|
||||
_titleLabel.text = "# " + tag.name
|
||||
guard let historys = tag.histories?.sorted(by: {
|
||||
$0.createAt.compare($1.createAt) == .orderedAscending
|
||||
}) else {
|
||||
_subTitleLabel.text = ""
|
||||
return
|
||||
}
|
||||
let recentHistory = historys.prefix(2)
|
||||
let peopleAreTalking = recentHistory.compactMap { Int($0.accounts) }.reduce(0, +)
|
||||
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
||||
_subTitleLabel.text = string
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct SearchingTableViewCell_Previews: PreviewProvider {
|
||||
static var controls: some View {
|
||||
Group {
|
||||
UIViewPreview {
|
||||
let cell = SearchingTableViewCell()
|
||||
cell.backgroundColor = .white
|
||||
cell._imageView.image = UIImage(systemName: "number.circle.fill")
|
||||
cell._titleLabel.text = "Electronic Frontier Foundation"
|
||||
cell._subTitleLabel.text = "@eff@mastodon.social"
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 228, height: 130))
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
controls.colorScheme(.light)
|
||||
controls.colorScheme(.dark)
|
||||
}
|
||||
.background(Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -25,10 +25,10 @@ class SearchRecommendCollectionHeader: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let seeAllButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
let seeAllButton: HighlightDimmableButton = {
|
||||
let button = HighlightDimmableButton(type: .custom)
|
||||
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
|
||||
button.setTitle(L10n.Scene.Search.Recommend.buttontext, for: .normal)
|
||||
button.setTitle(L10n.Scene.Search.Recommend.buttonText, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
|
|
|
@ -10,7 +10,31 @@ import UIKit
|
|||
// Make status bar style adptive for child view controller
|
||||
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
|
||||
final class AdaptiveStatusBarStyleNavigationController: UINavigationController {
|
||||
var viewControllersHiddenNavigationBar: [UIViewController.Type]
|
||||
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
return visibleViewController
|
||||
visibleViewController
|
||||
}
|
||||
|
||||
override init(rootViewController: UIViewController) {
|
||||
self.viewControllersHiddenNavigationBar = [SearchViewController.self]
|
||||
super.init(rootViewController: rootViewController)
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension AdaptiveStatusBarStyleNavigationController: UINavigationControllerDelegate {
|
||||
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
||||
let isContain = self.viewControllersHiddenNavigationBar.contains { type(of: viewController) == $0 }
|
||||
if isContain {
|
||||
self.setNavigationBarHidden(true, animated: animated)
|
||||
} else {
|
||||
self.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
// Created by sxiaojian on 2021/3/31.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import OSLog
|
||||
|
||||
extension APIService {
|
||||
|
||||
func recommendAccount(
|
||||
domain: String,
|
||||
query: Mastodon.API.Suggestions.Query?,
|
||||
|
@ -19,12 +21,33 @@ extension APIService {
|
|||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Suggestions.get(session: session, domain: domain, query: query, authorization: authorization)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> in
|
||||
let log = OSLog.api
|
||||
return self.backgroundManagedObjectContext.performChanges {
|
||||
response.value.forEach { user in
|
||||
let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: user, userCache: nil, networkDate: Date(), log: log)
|
||||
let flag = isCreated ? "+" : "-"
|
||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||
}
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Account]> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
func recommendTrends(
|
||||
domain: String,
|
||||
query: Mastodon.API.Trends.Query?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> {
|
||||
return Mastodon.API.Trends.get(session: session, domain: domain, query: query)
|
||||
Mastodon.API.Trends.get(session: session, domain: domain, query: query)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// APIService+CoreData+Tag.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/4/8.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService.CoreData {
|
||||
static func createOrMergeTag(
|
||||
into managedObjectContext: NSManagedObjectContext,
|
||||
entity: Mastodon.Entity.Tag
|
||||
) -> (Tag: Tag, isCreated: Bool) {
|
||||
// fetch old mastodon user
|
||||
let oldTag: Tag? = {
|
||||
let request = Tag.sortedFetchRequest
|
||||
request.predicate = Tag.predicate(name: entity.name)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
do {
|
||||
return try managedObjectContext.fetch(request).first
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
if let oldTag = oldTag {
|
||||
APIService.CoreData.merge(tag: oldTag, entity: entity, into: managedObjectContext)
|
||||
return (oldTag, false)
|
||||
} else {
|
||||
let histories = entity.history?.prefix(2).compactMap { history -> History in
|
||||
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
|
||||
}
|
||||
let tagInCoreData = Tag.insert(into: managedObjectContext, property: Tag.Property(name: entity.name, url: entity.url, histories: histories))
|
||||
return (tagInCoreData, true)
|
||||
}
|
||||
}
|
||||
|
||||
static func merge(tag: Tag, entity: Mastodon.Entity.Tag, into managedObjectContext: NSManagedObjectContext) {
|
||||
tag.update(url: tag.url)
|
||||
guard let tagHistories = tag.histories else { return }
|
||||
guard let entityHistories = entity.history?.prefix(2) else { return }
|
||||
let entityHistoriesCount = entityHistories.count
|
||||
if entityHistoriesCount == 0 {
|
||||
return
|
||||
}
|
||||
for n in 0 ..< tagHistories.count {
|
||||
if n < entityHistories.count {
|
||||
let entityHistory = entityHistories[n]
|
||||
tag.updateHistory(index: n, day: entityHistory.day, uses: entityHistory.uses, account: entityHistory.accounts)
|
||||
}
|
||||
}
|
||||
if entityHistoriesCount <= tagHistories.count {
|
||||
return
|
||||
}
|
||||
for n in 1 ... (entityHistoriesCount - tagHistories.count) {
|
||||
let entityHistory = entityHistories[entityHistoriesCount - n]
|
||||
tag.appendHistory(history: History.insert(into: managedObjectContext, property: History.Property(day: entityHistory.day, uses: entityHistory.uses, accounts: entityHistory.accounts)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,9 +50,6 @@ extension Mastodon.API.Search {
|
|||
}
|
||||
|
||||
extension Mastodon.API.Search {
|
||||
public enum SearchType: String, Codable {
|
||||
case ccounts, hashtags, statuses
|
||||
}
|
||||
|
||||
public struct Query: Codable, GetQuery {
|
||||
public init(q: String,
|
||||
|
@ -65,6 +62,7 @@ extension Mastodon.API.Search {
|
|||
limit: Int? = nil,
|
||||
offset: Int? = nil,
|
||||
following: Bool? = nil) {
|
||||
|
||||
self.accountID = accountID
|
||||
self.maxID = maxID
|
||||
self.minID = minID
|
||||
|
@ -106,3 +104,25 @@ extension Mastodon.API.Search {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Mastodon.API.Search {
|
||||
enum SearchType: String, Codable {
|
||||
case accounts
|
||||
case hashtags
|
||||
case statuses
|
||||
case `default`
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .accounts:
|
||||
return "accounts"
|
||||
case .hashtags:
|
||||
return "hashtags"
|
||||
case .statuses:
|
||||
return "statuses"
|
||||
case .default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
import Foundation
|
||||
extension Mastodon.Entity {
|
||||
public struct SearchResult: Codable {
|
||||
public init(accounts: [Mastodon.Entity.Account], statuses: [Mastodon.Entity.Status], hashtags: [Mastodon.Entity.Tag]) {
|
||||
self.accounts = accounts
|
||||
self.statuses = statuses
|
||||
self.hashtags = hashtags
|
||||
}
|
||||
|
||||
public let accounts: [Mastodon.Entity.Account]
|
||||
public let statuses: [Mastodon.Entity.Status]
|
||||
public let hashtags: [Mastodon.Entity.Tag]
|
||||
|
|
Loading…
Reference in New Issue