From 6e10efc490a30b5dd9420727b340403eb6c4261b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 2 Apr 2021 16:24:00 +0800 Subject: [PATCH 01/20] feature:searching page feature: searching Page --- Localization/app.json | 11 +- Mastodon.xcodeproj/project.pbxproj | 32 ++++- .../Diffiable/Item/SearchResultItem.swift | 39 ++++++ .../Section/SearchResultSection.swift | 32 +++++ Mastodon/Generated/Strings.swift | 16 ++- .../Resources/en.lproj/Localizable.strings | 7 +- ...earchRecommendTagsCollectionViewCell.swift | 9 +- ...ft => SearchViewController+Recomend.swift} | 2 +- .../SearchViewController+Searching.swift | 67 ++++++++++ .../Scene/Search/SearchViewController.swift | 32 +++++ Mastodon/Scene/Search/SearchViewModel.swift | 39 ++++++ .../SearchingTableViewCell.swift | 120 ++++++++++++++++++ .../SearchRecommendCollectionHeader.swift | 2 +- .../Entity/Mastodon+Entity+SearchResult.swift | 6 +- 14 files changed, 394 insertions(+), 20 deletions(-) create mode 100644 Mastodon/Diffiable/Item/SearchResultItem.swift create mode 100644 Mastodon/Diffiable/Section/SearchResultSection.swift rename Mastodon/Scene/Search/{SearchViewController+RecomendView.swift => SearchViewController+Recomend.swift} (99%) create mode 100644 Mastodon/Scene/Search/SearchViewController+Searching.swift create mode 100644 Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift diff --git a/Localization/app.json b/Localization/app.json index 6d96fd5bd..3ee1c8d54 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -265,7 +265,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", @@ -276,6 +276,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" } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index a2ab8dd41..9e4a97545 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.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 */; }; 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 */; }; @@ -30,7 +32,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+Recomend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.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 */; }; @@ -104,6 +106,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 */; }; @@ -366,6 +370,8 @@ 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; + 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; + 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = ""; }; 2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -374,7 +380,7 @@ 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; - 2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+RecomendView.swift"; sourceTree = ""; }; + 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Recomend.swift"; sourceTree = ""; }; 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = ""; }; 2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = ""; }; 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = ""; }; @@ -445,6 +451,8 @@ 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = ""; }; 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = ""; }; 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = ""; }; + 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = ""; }; + 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = ""; }; 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = ""; }; 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 = ""; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -943,6 +951,7 @@ DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, 2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */, 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, + 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, ); @@ -991,6 +1000,7 @@ isa = PBXGroup; children = ( 2D7631B225C159F700929FB9 /* Item.swift */, + 2D198642261BF09500F0B013 /* SearchResultItem.swift */, DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, DB1E347725F519300079D7DF /* PickServerItem.swift */, DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, @@ -1025,6 +1035,14 @@ path = Stack; sourceTree = ""; }; + 2DFAD5212616F8E300F9EE7C /* TableViewCell */ = { + isa = PBXGroup; + children = ( + 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */, + ); + path = TableViewCell; + sourceTree = ""; + }; 3FE14AD363ED19AE7FF210A6 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1508,9 +1526,11 @@ DB9D6BEE25E4F5370051B173 /* Search */ = { isa = PBXGroup; children = ( + 2DFAD5212616F8E300F9EE7C /* TableViewCell */, 2DE0FAC62615F5D200CDF649 /* View */, DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, - 2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */, + 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */, + 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, 2D34D9E026149C550081BFC0 /* CollectionViewCell */, ); @@ -2034,6 +2054,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 */, @@ -2063,6 +2084,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 */, @@ -2106,6 +2128,7 @@ DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */, DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.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 */, @@ -2180,7 +2203,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+Recomend.swift in Sources */, DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */, @@ -2235,6 +2258,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 */, diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift new file mode 100644 index 000000000..a0b5fe253 --- /dev/null +++ b/Mastodon/Diffiable/Item/SearchResultItem.swift @@ -0,0 +1,39 @@ +// +// SearchResultItem.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/6. +// + +import Foundation +import MastodonSDK + +enum SearchResultItem { + case hashTag(tag: Mastodon.Entity.Tag) + + case account(account: Mastodon.Entity.Account) +} + +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 + 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) + } + } +} diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift new file mode 100644 index 000000000..9c481a53a --- /dev/null +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -0,0 +1,32 @@ +// +// SearchResultSection.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/6. +// + +import Foundation +import MastodonSDK +import UIKit + +enum SearchResultSection: Equatable, Hashable { + case account + case hashTag +} + +extension SearchResultSection { + static func tableViewDiffableDataSource( + for tableView: UITableView + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell + switch result { + case .account(let account): + cell.config(with: account) + case .hashTag(let tag): + cell.config(with: tag) + } + return cell + } + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 0ce6bf212..05386714e 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -408,7 +408,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") @@ -434,6 +434,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. diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index f0ac3d44b..662491b2e 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -132,12 +132,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"; diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 685d214e6..7167658c0 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -82,14 +82,7 @@ extension SearchRecommendTagsCollectionViewCell { 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[0...2] let peopleAreTalking = recentHistory.compactMap({ Int($0.accounts) }).reduce(0, +) let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) peopleLabel.text = string diff --git a/Mastodon/Scene/Search/SearchViewController+RecomendView.swift b/Mastodon/Scene/Search/SearchViewController+Recomend.swift similarity index 99% rename from Mastodon/Scene/Search/SearchViewController+RecomendView.swift rename to Mastodon/Scene/Search/SearchViewController+Recomend.swift index ca373b6b5..ff6ba7d17 100644 --- a/Mastodon/Scene/Search/SearchViewController+RecomendView.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recomend.swift @@ -1,5 +1,5 @@ // -// SearchViewController+RecomendView.swift +// SearchViewController+Recomend.swift // Mastodon // // Created by sxiaojian on 2021/3/31. diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift new file mode 100644 index 000000000..b46714832 --- /dev/null +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -0,0 +1,67 @@ +// +// SearchViewController+Searching.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/2. +// + +import Foundation +import UIKit + +extension SearchViewController { + func setupSearchingTableView() { + searchingTableView.delegate = self + searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self)) + view.addSubview(searchingTableView) + searchingTableView.constrain([ + searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + 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), + ]) + + viewModel.isSearching + .receive(on: DispatchQueue.main) + .sink {[weak self] isSearching in + self?.searchingTableView.isHidden = !isSearching + if !isSearching { + self?.searchResultDiffableDataSource = nil + } + } + .store(in: &disposeBag) + + viewModel.searchResult + .receive(on: DispatchQueue.main) + .sink { [weak self] searchResult in + guard let self = self else { return } + let dataSource = SearchResultSection.tableViewDiffableDataSource(for: self.searchingTableView) + var snapshot = NSDiffableDataSourceSnapshot() + if let accounts = searchResult?.accounts { + snapshot.appendSections([.account]) + let items = accounts.compactMap { SearchResultItem.account(account: $0) } + snapshot.appendItems(items, toSection: .account) + } + if let tags = searchResult?.hashtags { + snapshot.appendSections([.hashTag]) + let items = tags.compactMap { SearchResultItem.hashTag(tag: $0) } + snapshot.appendItems(items, toSection: .hashTag) + } + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + self.searchResultDiffableDataSource = dataSource + } + .store(in: &disposeBag) + } +} + +// 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) {} +} diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 856407f33..a0a25fef6 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -24,9 +24,12 @@ final class SearchViewController: UIViewController, NeedsDependency { let micImage = UIImage(systemName: "mic.fill") searchBar.setImage(micImage, for: .bookmark, state: .normal) searchBar.showsBookmarkButton = true + searchBar.showsScopeBar = false + searchBar.scopeButtonTitles = [L10n.Scene.Search.Searching.Segment.all, L10n.Scene.Search.Searching.Segment.people,L10n.Scene.Search.Searching.Segment.hashtags] return searchBar }() + // recommend let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false @@ -71,6 +74,16 @@ final class SearchViewController: UIViewController, NeedsDependency { view.translatesAutoresizingMaskIntoConstraints = false return view }() + + // searching + let searchingTableView: UITableView = { + let tableView = UITableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .singleLine + tableView.backgroundColor = .white + return tableView + }() + var searchResultDiffableDataSource: UITableViewDiffableDataSource? } extension SearchViewController { @@ -83,6 +96,7 @@ extension SearchViewController { setupScrollView() setupHashTagCollectionView() setupAccountsCollectionView() + setupSearchingTableView() } func setupScrollView() { @@ -109,22 +123,40 @@ extension SearchViewController { 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 = "" + case 1: + viewModel.searchScope.value = "accounts" + case 2: + viewModel.searchScope.value = "hashtags" + default: + break + } + } func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {} } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 40b22c880..03f689a1b 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -19,12 +19,51 @@ final class SearchViewModel { // output let searchText = CurrentValueSubject("") + let searchScope = CurrentValueSubject("") + + let isSearching = CurrentValueSubject(false) + + let searchResult = CurrentValueSubject(nil) var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [Mastodon.Entity.Account]() init(context: AppContext) { self.context = context + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + Publishers.CombineLatest( + searchText + .filter { !$0.isEmpty } + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), + searchScope) + .flatMap { (text, scope) -> AnyPublisher, Error> in + let query = Mastodon.API.Search.Query(accountID: nil, + maxID: nil, + minID: nil, + type: scope, + excludeUnreviewed: nil, + q: text, + 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 + } + } + .store(in: &disposeBag) } func requestRecommendHashTags() -> Future { diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift new file mode 100644 index 000000000..ceb35678a --- /dev/null +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -0,0 +1,120 @@ +// +// SearchingTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/2. +// + +import Foundation +import UIKit +import MastodonSDK + +final class SearchingTableViewCell: UITableViewCell { + let _imageView: UIImageView = { + let imageView = UIImageView() + imageView.tintColor = .black + return imageView + }() + + let _titleLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.buttonDefault.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() { + self.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) { + self._imageView.af.setImage( + withURL: URL(string: account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + self._titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName + self._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) + self._imageView.image = image + self._titleLabel.text = "# " + tag.name + guard let historys = tag.history else { + self._subTitleLabel.text = "" + return + } + let recentHistory = historys[0...2] + let peopleAreTalking = recentHistory.compactMap({ Int($0.accounts) }).reduce(0, +) + let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) + self._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 diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index 00efecd85..193da2c47 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -28,7 +28,7 @@ class SearchRecommendCollectionHeader: UIView { let seeAllButton: UIButton = { let button = UIButton(type: .custom) button.setTitleColor(Asset.Colors.buttonDefault.color, for: .normal) - button.setTitle(L10n.Scene.Search.Recommend.buttontext, for: .normal) + button.setTitle(L10n.Scene.Search.Recommend.buttonText, for: .normal) return button }() diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift index f06f1a54e..f10339664 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift @@ -8,9 +8,9 @@ import Foundation extension Mastodon.Entity { public struct SearchResult: Codable { - let accounts: [Mastodon.Entity.Account] - let statuses: [Mastodon.Entity.Status] - let hashtags: [Mastodon.Entity.Tag] + public let accounts: [Mastodon.Entity.Account] + public let statuses: [Mastodon.Entity.Status] + public let hashtags: [Mastodon.Entity.Tag] } } From 90803fc5441175501e6197f512e1b247af2e3813 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 6 Apr 2021 15:25:04 +0800 Subject: [PATCH 02/20] chore: add bottom loader --- Mastodon.xcodeproj/project.pbxproj | 20 ++- .../Diffiable/Item/SearchResultItem.swift | 8 +- ...on.swift => RecommendHashTagSection.swift} | 8 +- .../Section/SearchResultSection.swift | 11 +- Mastodon/Extension/Array.swift | 20 +++ .../SearchViewController+Recomend.swift | 32 ---- .../SearchViewController+Searching.swift | 32 +--- .../Scene/Search/SearchViewController.swift | 34 ++++- .../SearchViewModel+LoadOldestState.swift | 141 ++++++++++++++++++ Mastodon/Scene/Search/SearchViewModel.swift | 79 +++++++++- .../TableViewCell/SearchBottomLoader.swift | 47 ++++++ .../SearchingTableViewCell.swift | 26 ++-- .../MastodonSDK/API/Mastodon+API+Search.swift | 20 ++- .../Entity/Mastodon+Entity+SearchResult.swift | 6 + 14 files changed, 392 insertions(+), 92 deletions(-) rename Mastodon/Diffiable/Section/{RecomendHashTagSection.swift => RecommendHashTagSection.swift} (74%) create mode 100644 Mastodon/Extension/Array.swift create mode 100644 Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift create mode 100644 Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9e4a97545..165b9af31 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -20,10 +20,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 */; }; + 2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A0A261D5A5600B44727 /* Array.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 */; }; @@ -97,7 +100,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 */; }; @@ -368,10 +371,13 @@ 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = ""; }; 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; + 2D0B7A0A261D5A5600B44727 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; + 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBottomLoader.swift; sourceTree = ""; }; + 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = ""; }; 2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -442,7 +448,7 @@ 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = ""; }; - 2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecomendHashTagSection.swift; sourceTree = ""; }; + 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = ""; }; 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = ""; }; 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = ""; }; 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; @@ -949,7 +955,7 @@ DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, - 2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */, + 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */, 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, @@ -1039,6 +1045,7 @@ isa = PBXGroup; children = ( 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */, + 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */, ); path = TableViewCell; sourceTree = ""; @@ -1470,6 +1477,7 @@ 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, + 2D0B7A0A261D5A5600B44727 /* Array.swift */, DB68A06225E905E000CFDF14 /* UIApplication.swift */, DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */, 2D42FF8E25C8228A004A627A /* UIButton.swift */, @@ -1532,6 +1540,7 @@ 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */, 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, + 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */, 2D34D9E026149C550081BFC0 /* CollectionViewCell */, ); path = Search; @@ -2097,6 +2106,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 */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */, @@ -2135,7 +2145,7 @@ 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 */, @@ -2179,6 +2189,7 @@ 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, + 2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, @@ -2245,6 +2256,7 @@ DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, + 2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */, 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */, 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift index a0b5fe253..1156a05fa 100644 --- a/Mastodon/Diffiable/Item/SearchResultItem.swift +++ b/Mastodon/Diffiable/Item/SearchResultItem.swift @@ -12,6 +12,8 @@ enum SearchResultItem { case hashTag(tag: Mastodon.Entity.Tag) case account(account: Mastodon.Entity.Account) + + case bottomLoader } extension SearchResultItem: Equatable { @@ -19,8 +21,10 @@ extension SearchResultItem: Equatable { switch (lhs, rhs) { case (.hashTag(let tagLeft), .hashTag(let tagRight)): return tagLeft == tagRight - case (.account(let accountLeft), account(let accountRight)): + case (.account(let accountLeft), .account(let accountRight)): return accountLeft == accountRight + case (.bottomLoader, .bottomLoader): + return true default: return false } @@ -34,6 +38,8 @@ extension SearchResultItem: Hashable { hasher.combine(account) case .hashTag(let tag): hasher.combine(tag) + case .bottomLoader: + hasher.combine(String(describing: SearchResultItem.bottomLoader.self)) } } } diff --git a/Mastodon/Diffiable/Section/RecomendHashTagSection.swift b/Mastodon/Diffiable/Section/RecommendHashTagSection.swift similarity index 74% rename from Mastodon/Diffiable/Section/RecomendHashTagSection.swift rename to Mastodon/Diffiable/Section/RecommendHashTagSection.swift index 2f78e73b9..502086910 100644 --- a/Mastodon/Diffiable/Section/RecomendHashTagSection.swift +++ b/Mastodon/Diffiable/Section/RecommendHashTagSection.swift @@ -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 { + ) -> UICollectionViewDiffableDataSource { 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) diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift index 9c481a53a..91e443bdc 100644 --- a/Mastodon/Diffiable/Section/SearchResultSection.swift +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -12,6 +12,7 @@ import UIKit enum SearchResultSection: Equatable, Hashable { case account case hashTag + case bottomLoader } extension SearchResultSection { @@ -19,14 +20,20 @@ extension SearchResultSection { for tableView: UITableView ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell 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 .bottomLoader: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader + cell.startAnimating() + return cell } - return cell } } } diff --git a/Mastodon/Extension/Array.swift b/Mastodon/Extension/Array.swift new file mode 100644 index 000000000..91c2e3d66 --- /dev/null +++ b/Mastodon/Extension/Array.swift @@ -0,0 +1,20 @@ +// +// Array.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/7. +// + +import Foundation + +public extension Array where Element: Equatable { + + func removeDuplicate() -> Array { + return self.enumerated().filter { (index,value) -> Bool in + return self.firstIndex(of: value) == index + }.map { (_, value) in + value + } + } +} + diff --git a/Mastodon/Scene/Search/SearchViewController+Recomend.swift b/Mastodon/Scene/Search/SearchViewController+Recomend.swift index ff6ba7d17..f2e916ab7 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recomend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recomend.swift @@ -25,22 +25,6 @@ extension SearchViewController { 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() - 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,22 +41,6 @@ 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() - 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() { diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index b46714832..51281f30c 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -6,12 +6,14 @@ // import Foundation +import MastodonSDK 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: view.safeAreaLayoutGuide.topAnchor), @@ -20,35 +22,11 @@ extension SearchViewController { 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 + .sink { [weak self] isSearching in self?.searchingTableView.isHidden = !isSearching - if !isSearching { - self?.searchResultDiffableDataSource = nil - } - } - .store(in: &disposeBag) - - viewModel.searchResult - .receive(on: DispatchQueue.main) - .sink { [weak self] searchResult in - guard let self = self else { return } - let dataSource = SearchResultSection.tableViewDiffableDataSource(for: self.searchingTableView) - var snapshot = NSDiffableDataSourceSnapshot() - if let accounts = searchResult?.accounts { - snapshot.appendSections([.account]) - let items = accounts.compactMap { SearchResultItem.account(account: $0) } - snapshot.appendItems(items, toSection: .account) - } - if let tags = searchResult?.hashtags { - snapshot.appendSections([.hashTag]) - let items = tags.compactMap { SearchResultItem.hashTag(tag: $0) } - snapshot.appendItems(items, toSection: .hashTag) - } - dataSource.apply(snapshot, animatingDifferences: false, completion: nil) - self.searchResultDiffableDataSource = dataSource } .store(in: &disposeBag) } @@ -60,8 +38,10 @@ 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) {} } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index a0a25fef6..0b26cdb40 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -6,6 +6,7 @@ // import Combine +import GameplayKit import MastodonSDK import UIKit @@ -25,7 +26,7 @@ final class SearchViewController: UIViewController, NeedsDependency { searchBar.setImage(micImage, for: .bookmark, state: .normal) searchBar.showsBookmarkButton = true searchBar.showsScopeBar = false - searchBar.scopeButtonTitles = [L10n.Scene.Search.Searching.Segment.all, L10n.Scene.Search.Searching.Segment.people,L10n.Scene.Search.Searching.Segment.hashtags] + searchBar.scopeButtonTitles = [L10n.Scene.Search.Searching.Segment.all, L10n.Scene.Search.Searching.Segment.people, L10n.Scene.Search.Searching.Segment.hashtags] return searchBar }() @@ -59,9 +60,6 @@ final class SearchViewController: UIViewController, NeedsDependency { view.translatesAutoresizingMaskIntoConstraints = false return view }() - - var hashTagDiffableDataSource: UICollectionViewDiffableDataSource? - var accountDiffableDataSource: UICollectionViewDiffableDataSource? let accountsCollectionView: UICollectionView = { let flowLayout = UICollectionViewFlowLayout() @@ -83,7 +81,6 @@ final class SearchViewController: UIViewController, NeedsDependency { tableView.backgroundColor = .white return tableView }() - var searchResultDiffableDataSource: UITableViewDiffableDataSource? } extension SearchViewController { @@ -97,6 +94,7 @@ extension SearchViewController { setupHashTagCollectionView() setupAccountsCollectionView() setupSearchingTableView() + setupDataSource() } func setupScrollView() { @@ -118,6 +116,20 @@ extension SearchViewController { scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), ]) } + + func setupDataSource() { + viewModel.hashTagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashTagCollectionView) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView) + viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView) + } +} + +extension SearchViewController: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView == searchingTableView { + handleScrollViewDidScroll(scrollView) + } + } } extension SearchViewController: UISearchBarDelegate { @@ -150,16 +162,24 @@ extension SearchViewController: UISearchBarDelegate { case 0: viewModel.searchScope.value = "" case 1: - viewModel.searchScope.value = "accounts" + viewModel.searchScope.value = Mastodon.API.Search.Scope.accounts.rawValue case 2: - viewModel.searchScope.value = "hashtags" + viewModel.searchScope.value = Mastodon.API.Search.Scope.hashTags.rawValue 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 diff --git a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift new file mode 100644 index 000000000..fb57b6d34 --- /dev/null +++ b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift @@ -0,0 +1,141 @@ +// +// 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.Scope.accounts.rawValue: + offset = oldSearchResult.accounts.count + case Mastodon.API.Search.Scope.hashTags.rawValue: + offset = oldSearchResult.hashtags.count + default: + return + } + let query = Mastodon.API.Search.Query(accountID: nil, + maxID: nil, + minID: nil, + type: viewModel.searchScope.value, + excludeUnreviewed: nil, + q: viewModel.searchText.value, + 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.Scope.accounts.rawValue: + 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) + viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: newAccounts.removeDuplicate(), statuses: oldSearchResult.statuses, hashtags: oldSearchResult.hashtags) + stateMachine.enter(Idle.self) + } + case Mastodon.API.Search.Scope.hashTags.rawValue: + 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) + viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: oldSearchResult.accounts, statuses: oldSearchResult.statuses, hashtags: newTags.removeDuplicate()) + 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) + } + } +} diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 03f689a1b..4fbdab5ba 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import GameplayKit import MastodonSDK import OSLog import UIKit @@ -28,6 +29,26 @@ final class SearchViewModel { var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [Mastodon.Entity.Account]() + var hashTagDiffableDataSource: UICollectionViewDiffableDataSource? + var accountDiffableDataSource: UICollectionViewDiffableDataSource? + var searchResultDiffableDataSource: UITableViewDiffableDataSource? + + // 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(nil) + init(context: AppContext) { self.context = context guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { @@ -60,10 +81,66 @@ final class SearchViewModel { isSearching .sink { [weak self] isSearching in if !isSearching { - self?.searchResult.value == nil + self?.searchResult.value = 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() + 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() + 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() + 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.Scope.accounts.rawValue { + 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.Scope.hashTags.rawValue { + snapshot.appendItems([.bottomLoader], toSection: .hashTag) + } + } + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + .store(in: &disposeBag) } func requestRecommendHashTags() -> Future { diff --git a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift b/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift new file mode 100644 index 000000000..dcd4c4971 --- /dev/null +++ b/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift @@ -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.lightWhite.color + contentView.addSubview(activityIndicatorView) + activityIndicatorView.constrainToCenter() + } +} diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index ceb35678a..379d720ea 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -6,8 +6,8 @@ // import Foundation -import UIKit import MastodonSDK +import UIKit final class SearchingTableViewCell: UITableViewCell { let _imageView: UIImageView = { @@ -50,7 +50,7 @@ final class SearchingTableViewCell: UITableViewCell { extension SearchingTableViewCell { private func configure() { - self.selectionStyle = .none + selectionStyle = .none contentView.addSubview(_imageView) _imageView.pin(toSize: CGSize(width: 42, height: 42)) _imageView.constrain([ @@ -65,28 +65,28 @@ extension SearchingTableViewCell { _subTitleLabel.pin(top: 34, left: 75, bottom: nil, right: 0) } - func config(with account:Mastodon.Entity.Account) { - self._imageView.af.setImage( + func config(with account: Mastodon.Entity.Account) { + _imageView.af.setImage( withURL: URL(string: account.avatar)!, placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) - self._titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName - self._subTitleLabel.text = account.acct + _titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName + _subTitleLabel.text = account.acct } - func config(with tag:Mastodon.Entity.Tag) { + func config(with tag: Mastodon.Entity.Tag) { let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) - self._imageView.image = image - self._titleLabel.text = "# " + tag.name + _imageView.image = image + _titleLabel.text = "# " + tag.name guard let historys = tag.history else { - self._subTitleLabel.text = "" + _subTitleLabel.text = "" return } - let recentHistory = historys[0...2] - let peopleAreTalking = recentHistory.compactMap({ Int($0.accounts) }).reduce(0, +) + let recentHistory = historys[0 ... 2] + let peopleAreTalking = recentHistory.compactMap { Int($0.accounts) }.reduce(0, +) let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) - self._subTitleLabel.text = string + _subTitleLabel.text = string } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift index 8f266437f..465c133f2 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift @@ -49,8 +49,8 @@ extension Mastodon.API.Search { } } -extension Mastodon.API.Search { - public struct Query: Codable, GetQuery { +public extension Mastodon.API.Search { + struct Query: Codable, GetQuery { public init(accountID: Mastodon.Entity.Account.ID?, maxID: Mastodon.Entity.Status.ID?, minID: Mastodon.Entity.Status.ID?, type: String?, excludeUnreviewed: Bool?, q: String, resolve: Bool?, limit: Int?, offset: Int?, following: Bool?) { self.accountID = accountID self.maxID = maxID @@ -93,3 +93,19 @@ extension Mastodon.API.Search { } } } + +public extension Mastodon.API.Search { + enum Scope: String { + case accounts + case hashTags + + public var rawValue: String { + switch self { + case .accounts: + return "accounts" + case .hashTags: + return "hashtags" + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift index f10339664..44446d0d9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift @@ -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] From d800e10bd7b069f61b812730695618a357330f7d Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 7 Apr 2021 19:49:33 +0800 Subject: [PATCH 03/20] feature: add search history --- .../CoreData.xcdatamodel/contents | 17 +- CoreDataStack/Entity/SearchHistory.swift | 54 +++++++ Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Item/SearchResultItem.swift | 13 ++ .../Section/SearchResultSection.swift | 16 +- .../SearchViewController+Searching.swift | 48 +++++- .../Scene/Search/SearchViewController.swift | 29 +++- Mastodon/Scene/Search/SearchViewModel.swift | 145 +++++++++++++++--- .../SearchingTableViewCell.swift | 31 +++- .../SearchRecommendCollectionHeader.swift | 2 +- 10 files changed, 325 insertions(+), 34 deletions(-) create mode 100644 CoreDataStack/Entity/SearchHistory.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index d655753d3..5f048880f 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -138,12 +138,18 @@ - + + + + + + + @@ -201,8 +207,9 @@ - - + + + - + \ No newline at end of file diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift new file mode 100644 index 000000000..c1a81bc05 --- /dev/null +++ b/CoreDataStack/Entity/SearchHistory.swift @@ -0,0 +1,54 @@ +// +// 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 account: MastodonUser? + @NSManaged public private(set) var hashTag: Tag? + +} + +extension SearchHistory { + public override func awakeFromInsert() { + super.awakeFromInsert() + setPrimitiveValue(UUID(), forKey: #keyPath(SearchHistory.identifier)) + } + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + account: MastodonUser + ) -> SearchHistory { + let searchHistory: SearchHistory = context.insertObject() + searchHistory.account = account + searchHistory.createAt = Date() + return searchHistory + } + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + hashTag: Tag + ) -> SearchHistory { + let searchHistory: SearchHistory = context.insertObject() + searchHistory.hashTag = hashTag + searchHistory.createAt = Date() + return searchHistory + } +} + +extension SearchHistory: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \SearchHistory.createAt, ascending: false)] + } +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 165b9af31..214aece57 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 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 */; }; 2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A0A261D5A5600B44727 /* Array.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 */; }; @@ -372,6 +373,7 @@ 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; 2D0B7A0A261D5A5600B44727 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; + 2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; @@ -1398,6 +1400,7 @@ isa = PBXGroup; children = ( DB89BA2625C110B4008580ED /* Status.swift */, + 2D0B7A1C261D839600B44727 /* SearchHistory.swift */, DB8AF52425C131D1002E6C99 /* MastodonUser.swift */, DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */, 2D927F0125C7E4F2004F19B8 /* Mention.swift */, @@ -2333,6 +2336,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; diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift index 1156a05fa..56390f203 100644 --- a/Mastodon/Diffiable/Item/SearchResultItem.swift +++ b/Mastodon/Diffiable/Item/SearchResultItem.swift @@ -5,6 +5,7 @@ // Created by sxiaojian on 2021/4/6. // +import CoreData import Foundation import MastodonSDK @@ -13,6 +14,10 @@ enum SearchResultItem { case account(account: Mastodon.Entity.Account) + case accountObjectID(accountObjectID: NSManagedObjectID) + + case hashTagObjectID(hashTagObjectID: NSManagedObjectID) + case bottomLoader } @@ -25,6 +30,10 @@ extension SearchResultItem: Equatable { 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 } @@ -38,6 +47,10 @@ extension SearchResultItem: Hashable { 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)) } diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift index 91e443bdc..66f6891e4 100644 --- a/Mastodon/Diffiable/Section/SearchResultSection.swift +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -8,16 +8,20 @@ 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 + for tableView: UITableView, + dependency: NeedsDependency ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, result) -> UITableViewCell? in switch result { @@ -29,6 +33,16 @@ extension SearchResultSection { 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() diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 51281f30c..34acf443f 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -5,8 +5,12 @@ // Created by sxiaojian on 2021/4/2. // +import Combine +import CoreData +import CoreDataStack import Foundation import MastodonSDK +import OSLog import UIKit extension SearchViewController { @@ -20,7 +24,7 @@ extension SearchViewController { 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.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor) ]) searchingTableView.tableFooterView = UIView() viewModel.isSearching @@ -29,6 +33,42 @@ extension SearchViewController { 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() } } @@ -43,5 +83,9 @@ extension SearchViewController: UITableViewDelegate { 66 } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + viewModel.saveItemToCoreData(item: item) + } } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 0b26cdb40..1dfa87e77 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -76,17 +76,39 @@ final class SearchViewController: UIViewController, NeedsDependency { // searching let searchingTableView: UITableView = { let tableView = UITableView() + tableView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .singleLine - tableView.backgroundColor = .white 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: UIButton = { + let button = UIButton(type: .custom) + button.setTitleColor(Asset.Colors.buttonDefault.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 + view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color searchBar.delegate = self navigationItem.titleView = searchBar navigationItem.hidesBackButton = true @@ -95,6 +117,7 @@ extension SearchViewController { setupAccountsCollectionView() setupSearchingTableView() setupDataSource() + setupSearchHeader() } func setupScrollView() { @@ -120,7 +143,7 @@ extension SearchViewController { func setupDataSource() { viewModel.hashTagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashTagCollectionView) viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView) - viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView) + viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 4fbdab5ba..6827c7035 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -6,13 +6,15 @@ // 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() // input @@ -51,41 +53,79 @@ final class SearchViewModel { init(context: AppContext) { self.context = context + super.init() + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } Publishers.CombineLatest( searchText - .filter { !$0.isEmpty } .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), - searchScope) - .flatMap { (text, scope) -> AnyPublisher, Error> in - let query = Mastodon.API.Search.Query(accountID: nil, - maxID: nil, - minID: nil, - type: scope, - excludeUnreviewed: nil, - q: text, - 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) + searchScope + ) + .filter { text, _ in + !text.isEmpty + } + .flatMap { (text, scope) -> AnyPublisher, Error> in + + let query = Mastodon.API.Search.Query(accountID: nil, + maxID: nil, + minID: nil, + type: scope, + excludeUnreviewed: nil, + q: text, + 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, text, _ in + isSearching && text.isEmpty + } + .sink { [weak self] _, _, scope in + guard let self = self else { return } + guard let searchHistories = self.fetchSearchHistory() else { return } + guard let dataSource = self.searchResultDiffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.mixed]) + + searchHistories.forEach { searchHistory in + let containsAccount = scope == Mastodon.API.Search.Scope.accounts.rawValue || scope == "" + let containsHashTag = scope == Mastodon.API.Search.Scope.hashTags.rawValue || scope == "" + 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 @@ -190,4 +230,67 @@ final class SearchViewModel { .store(in: &self.disposeBag) } } + + func saveItemToCoreData(item: SearchResultItem) { + _ = 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) + SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser) + + case .hashTag(let tag): + let histories = tag.history?[0 ... 2].compactMap { history -> History in + History.insert(into: self.context.managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) + } + let tagInCoreData = Tag.insert(into: self.context.managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) + SearchHistory.insert(into: self.context.managedObjectContext, hashTag: tagInCoreData) + + default: + break + } + } + } + + func fetchSearchHistory() -> [SearchHistory]? { + let searchHistory: [SearchHistory]? = { + let request = SearchHistory.sortedFetchRequest + request.predicate = nil + request.returnsObjectsAsFaults = false + do { + return try context.managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return nil + } + + }() + return searchHistory + } + + func deleteSearchHistory() { + let result = fetchSearchHistory() + _ = context.managedObjectContext.performChanges { [weak self] in + result?.forEach { history in + self?.context.managedObjectContext.delete(history) + } + self?.isSearching.value = true + } + } } diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index 379d720ea..cdfcdce23 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -5,6 +5,8 @@ // Created by sxiaojian on 2021/4/2. // +import CoreData +import CoreDataStack import Foundation import MastodonSDK import UIKit @@ -12,7 +14,7 @@ import UIKit final class SearchingTableViewCell: UITableViewCell { let _imageView: UIImageView = { let imageView = UIImageView() - imageView.tintColor = .black + imageView.tintColor = Asset.Colors.Label.primary.color return imageView }() @@ -50,6 +52,7 @@ final class SearchingTableViewCell: UITableViewCell { extension SearchingTableViewCell { private func configure() { + backgroundColor = .clear selectionStyle = .none contentView.addSubview(_imageView) _imageView.pin(toSize: CGSize(width: 42, height: 42)) @@ -75,6 +78,16 @@ extension SearchingTableViewCell { _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 @@ -88,6 +101,22 @@ extension SearchingTableViewCell { 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[0 ... 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 diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index 193da2c47..ebd60ac30 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -18,7 +18,7 @@ class SearchRecommendCollectionHeader: UIView { let descriptionLabel: UILabel = { let label = UILabel() - label.textColor = Asset.Colors.lightSecondaryText.color + label.textColor = Asset.Colors.Label.secondary.color label.font = .preferredFont(forTextStyle: .body) label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping From 33016d9cf479b250f4c5d3a492b3dc195d0e60d0 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 7 Apr 2021 21:01:32 +0800 Subject: [PATCH 04/20] chore: rename hashTag to hashtag --- .../CoreData.xcdatamodel/contents | 30 +++++++++---------- CoreDataStack/Entity/SearchHistory.swift | 6 ++-- .../Diffiable/Item/SearchResultItem.swift | 12 ++++---- .../Section/SearchResultSection.swift | 8 ++--- Mastodon/Generated/Assets.swift | 1 - .../Background/search.colorset/Contents.json | 20 ------------- ...earchRecommendTagsCollectionViewCell.swift | 10 +++---- .../SearchViewController+Recomend.swift | 20 ++++++------- .../Scene/Search/SearchViewController.swift | 6 ++-- .../SearchViewModel+LoadOldestState.swift | 4 +-- Mastodon/Scene/Search/SearchViewModel.swift | 26 ++++++++-------- .../MastodonSDK/API/Mastodon+API+Search.swift | 4 +-- 12 files changed, 63 insertions(+), 84 deletions(-) delete mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 5f048880f..3904eb9ec 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -148,7 +148,7 @@ - + @@ -197,19 +197,19 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift index c1a81bc05..33b8a6010 100644 --- a/CoreDataStack/Entity/SearchHistory.swift +++ b/CoreDataStack/Entity/SearchHistory.swift @@ -14,7 +14,7 @@ public final class SearchHistory: NSManagedObject { @NSManaged public private(set) var createAt: Date @NSManaged public private(set) var account: MastodonUser? - @NSManaged public private(set) var hashTag: Tag? + @NSManaged public private(set) var hashtag: Tag? } @@ -38,10 +38,10 @@ extension SearchHistory { @discardableResult public static func insert( into context: NSManagedObjectContext, - hashTag: Tag + hashtag: Tag ) -> SearchHistory { let searchHistory: SearchHistory = context.insertObject() - searchHistory.hashTag = hashTag + searchHistory.hashtag = hashtag searchHistory.createAt = Date() return searchHistory } diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift index 56390f203..53a36e2e5 100644 --- a/Mastodon/Diffiable/Item/SearchResultItem.swift +++ b/Mastodon/Diffiable/Item/SearchResultItem.swift @@ -10,13 +10,13 @@ import Foundation import MastodonSDK enum SearchResultItem { - case hashTag(tag: Mastodon.Entity.Tag) + case hashtag(tag: Mastodon.Entity.Tag) case account(account: Mastodon.Entity.Account) case accountObjectID(accountObjectID: NSManagedObjectID) - case hashTagObjectID(hashTagObjectID: NSManagedObjectID) + case hashtagObjectID(hashtagObjectID: NSManagedObjectID) case bottomLoader } @@ -24,7 +24,7 @@ enum SearchResultItem { extension SearchResultItem: Equatable { static func == (lhs: SearchResultItem, rhs: SearchResultItem) -> Bool { switch (lhs, rhs) { - case (.hashTag(let tagLeft), .hashTag(let tagRight)): + case (.hashtag(let tagLeft), .hashtag(let tagRight)): return tagLeft == tagRight case (.account(let accountLeft), .account(let accountRight)): return accountLeft == accountRight @@ -32,7 +32,7 @@ extension SearchResultItem: Equatable { return true case (.accountObjectID(let idLeft),.accountObjectID(let idRight)): return idLeft == idRight - case (.hashTagObjectID(let idLeft),.hashTagObjectID(let idRight)): + case (.hashtagObjectID(let idLeft),.hashtagObjectID(let idRight)): return idLeft == idRight default: return false @@ -45,11 +45,11 @@ extension SearchResultItem: Hashable { switch self { case .account(let account): hasher.combine(account) - case .hashTag(let tag): + case .hashtag(let tag): hasher.combine(tag) case .accountObjectID(let id): hasher.combine(id) - case .hashTagObjectID(let id): + case .hashtagObjectID(let id): hasher.combine(id) case .bottomLoader: hasher.combine(String(describing: SearchResultItem.bottomLoader.self)) diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift index 66f6891e4..50c561605 100644 --- a/Mastodon/Diffiable/Section/SearchResultSection.swift +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -13,7 +13,7 @@ import CoreDataStack enum SearchResultSection: Equatable, Hashable { case account - case hashTag + case hashtag case mixed case bottomLoader } @@ -29,13 +29,13 @@ extension SearchResultSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchingTableViewCell.self), for: indexPath) as! SearchingTableViewCell cell.config(with: account) return cell - case .hashTag(let tag): + 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): + 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 + let tag = dependency.context.managedObjectContext.object(with: hashtagObjectID) as! Tag cell.config(with: tag) return cell case .accountObjectID(let accountObjectID): diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 241388199..8276cfb20 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -43,7 +43,6 @@ internal enum Asset { internal static let danger = ColorAsset(name: "Colors/Background/danger") internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") - internal static let search = ColorAsset(name: "Colors/Background/search") 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 success = ColorAsset(name: "Colors/Background/success") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json deleted file mode 100644 index 838e44e44..000000000 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json +++ /dev/null @@ -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 - } -} diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 7167658c0..4fe87e3f8 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -16,7 +16,7 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { return imageView }() - let hashTagTitleLabel: UILabel = { + let hashtagTitleLabel: UILabel = { let label = UILabel() label.textColor = .white label.font = .systemFont(ofSize: 20, weight: .semibold) @@ -66,8 +66,8 @@ extension SearchRecommendTagsCollectionViewCell { 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,7 +77,7 @@ 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 @@ -98,7 +98,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 } diff --git a/Mastodon/Scene/Search/SearchViewController+Recomend.swift b/Mastodon/Scene/Search/SearchViewController+Recomend.swift index f2e916ab7..87f32261c 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recomend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recomend.swift @@ -15,15 +15,15 @@ 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) ]) } @@ -45,7 +45,7 @@ extension SearchViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - hashTagCollectionView.collectionViewLayout.invalidateLayout() + hashtagCollectionView.collectionViewLayout.invalidateLayout() accountsCollectionView.collectionViewLayout.invalidateLayout() } } @@ -65,7 +65,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 @@ -73,7 +73,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) @@ -82,7 +82,7 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout { } extension SearchViewController { - @objc func hashTagSeeAllButtonPressed(_ sender: UIButton) {} + @objc func hashtagSeeAllButtonPressed(_ sender: UIButton) {} @objc func accountSeeAllButtonPressed(_ sender: UIButton) {} } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 1dfa87e77..9a8ab4804 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -49,7 +49,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) @@ -141,7 +141,7 @@ extension SearchViewController { } func setupDataSource() { - viewModel.hashTagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashTagCollectionView) + viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView) viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } @@ -187,7 +187,7 @@ extension SearchViewController: UISearchBarDelegate { case 1: viewModel.searchScope.value = Mastodon.API.Search.Scope.accounts.rawValue case 2: - viewModel.searchScope.value = Mastodon.API.Search.Scope.hashTags.rawValue + viewModel.searchScope.value = Mastodon.API.Search.Scope.hashtags.rawValue default: break } diff --git a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift index fb57b6d34..6306e2e6c 100644 --- a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift @@ -55,7 +55,7 @@ extension SearchViewModel.LoadOldestState { switch viewModel.searchScope.value { case Mastodon.API.Search.Scope.accounts.rawValue: offset = oldSearchResult.accounts.count - case Mastodon.API.Search.Scope.hashTags.rawValue: + case Mastodon.API.Search.Scope.hashtags.rawValue: offset = oldSearchResult.hashtags.count default: return @@ -91,7 +91,7 @@ extension SearchViewModel.LoadOldestState { viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: newAccounts.removeDuplicate(), statuses: oldSearchResult.statuses, hashtags: oldSearchResult.hashtags) stateMachine.enter(Idle.self) } - case Mastodon.API.Search.Scope.hashTags.rawValue: + case Mastodon.API.Search.Scope.hashtags.rawValue: if result.value.hashtags.isEmpty { stateMachine.enter(NoMore.self) } else { diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 6827c7035..df13ac48c 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -31,7 +31,7 @@ final class SearchViewModel: NSObject { var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [Mastodon.Entity.Account]() - var hashTagDiffableDataSource: UICollectionViewDiffableDataSource? + var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? var accountDiffableDataSource: UICollectionViewDiffableDataSource? var searchResultDiffableDataSource: UITableViewDiffableDataSource? @@ -112,13 +112,13 @@ final class SearchViewModel: NSObject { searchHistories.forEach { searchHistory in let containsAccount = scope == Mastodon.API.Search.Scope.accounts.rawValue || scope == "" - let containsHashTag = scope == Mastodon.API.Search.Scope.hashTags.rawValue || scope == "" + let containsHashTag = scope == Mastodon.API.Search.Scope.hashtags.rawValue || scope == "" 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) + if let tag = searchHistory.hashtag, containsHashTag { + let item = SearchResultItem.hashtagObjectID(hashtagObjectID: tag.objectID) snapshot.appendItems([item], toSection: .mixed) } } @@ -131,7 +131,7 @@ final class SearchViewModel: NSObject { .sink { [weak self] _ in guard let self = self else { return } if !self.recommendHashTags.isEmpty { - guard let dataSource = self.hashTagDiffableDataSource else { return } + guard let dataSource = self.hashtagDiffableDataSource else { return } var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(self.recommendHashTags, toSection: .main) @@ -166,16 +166,16 @@ final class SearchViewModel: NSObject { snapshot.appendSections([.account]) let items = accounts.compactMap { SearchResultItem.account(account: $0) } snapshot.appendItems(items, toSection: .account) - if self.searchScope.value == Mastodon.API.Search.Scope.accounts.rawValue { + if self.searchScope.value == Mastodon.API.Search.Scope.accounts.rawValue && !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.Scope.hashTags.rawValue { - snapshot.appendItems([.bottomLoader], toSection: .hashTag) + snapshot.appendSections([.hashtag]) + let items = tags.compactMap { SearchResultItem.hashtag(tag: $0) } + snapshot.appendItems(items, toSection: .hashtag) + if self.searchScope.value == Mastodon.API.Search.Scope.hashtags.rawValue && !items.isEmpty { + snapshot.appendItems([.bottomLoader], toSection: .hashtag) } } dataSource.apply(snapshot, animatingDifferences: false, completion: nil) @@ -255,12 +255,12 @@ final class SearchViewModel: NSObject { let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.context.managedObjectContext, for: requestMastodonUser, in: activeMastodonAuthenticationBox.domain, entity: account, userCache: nil, networkDate: Date(), log: OSLog.api) SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser) - case .hashTag(let tag): + case .hashtag(let tag): let histories = tag.history?[0 ... 2].compactMap { history -> History in History.insert(into: self.context.managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) } let tagInCoreData = Tag.insert(into: self.context.managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) - SearchHistory.insert(into: self.context.managedObjectContext, hashTag: tagInCoreData) + SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData) default: break diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift index 465c133f2..d4b8e8045 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift @@ -97,13 +97,13 @@ public extension Mastodon.API.Search { public extension Mastodon.API.Search { enum Scope: String { case accounts - case hashTags + case hashtags public var rawValue: String { switch self { case .accounts: return "accounts" - case .hashTags: + case .hashtags: return "hashtags" } } From 27b698a97a3725974dcc5ad41c171da1b6cb1cdf Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 7 Apr 2021 21:42:43 +0800 Subject: [PATCH 05/20] chore: add backgroud.navigation.color. update colors in searching page --- Mastodon/Generated/Assets.swift | 1 + .../navigationBar.colorset/Contents.json | 38 +++++++++++++++++ .../Scene/Search/SearchViewController.swift | 18 +++++--- .../SearchViewModel+LoadOldestState.swift | 15 +++---- Mastodon/Scene/Search/SearchViewModel.swift | 42 ++++++++++--------- .../TableViewCell/SearchBottomLoader.swift | 2 +- .../SearchingTableViewCell.swift | 2 +- .../SearchRecommendCollectionHeader.swift | 2 +- .../MastodonSDK/API/Mastodon+API+Search.swift | 11 +++-- 9 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/navigationBar.colorset/Contents.json diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 71034c1d8..c8fccbfef 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -42,6 +42,7 @@ 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 secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/navigationBar.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/navigationBar.colorset/Contents.json new file mode 100644 index 000000000..7f9578a7a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/navigationBar.colorset/Contents.json @@ -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 + } +} diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 2e6fc651c..470c88e0d 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -27,6 +27,7 @@ final class SearchViewController: UIViewController, NeedsDependency { searchBar.showsBookmarkButton = true searchBar.showsScopeBar = false 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 }() @@ -76,9 +77,10 @@ final class SearchViewController: UIViewController, NeedsDependency { // searching let searchingTableView: UITableView = { let tableView = UITableView() - tableView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .singleLine + tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return tableView }() @@ -99,7 +101,7 @@ final class SearchViewController: UIViewController, NeedsDependency { let clearSearchHistoryButton: UIButton = { let button = UIButton(type: .custom) - button.setTitleColor(Asset.Colors.buttonDefault.color, for: .normal) + button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal) return button }() @@ -109,6 +111,12 @@ extension SearchViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithTransparentBackground() + barAppearance.backgroundColor = Asset.Colors.Background.navigationBar.color + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance searchBar.delegate = self navigationItem.titleView = searchBar navigationItem.hidesBackButton = true @@ -183,11 +191,11 @@ extension SearchViewController: UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { switch selectedScope { case 0: - viewModel.searchScope.value = "" + viewModel.searchScope.value = Mastodon.API.Search.SearchType.default case 1: - viewModel.searchScope.value = Mastodon.API.Search.Scope.accounts.rawValue + viewModel.searchScope.value = Mastodon.API.Search.SearchType.accounts case 2: - viewModel.searchScope.value = Mastodon.API.Search.Scope.hashtags.rawValue + viewModel.searchScope.value = Mastodon.API.Search.SearchType.hashtags default: break } diff --git a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift index 6306e2e6c..7088f1360 100644 --- a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift @@ -53,23 +53,24 @@ extension SearchViewModel.LoadOldestState { } var offset = 0 switch viewModel.searchScope.value { - case Mastodon.API.Search.Scope.accounts.rawValue: + case Mastodon.API.Search.SearchType.accounts: offset = oldSearchResult.accounts.count - case Mastodon.API.Search.Scope.hashtags.rawValue: + case Mastodon.API.Search.SearchType.hashtags: offset = oldSearchResult.hashtags.count default: return } - let query = Mastodon.API.Search.Query(accountID: nil, + let query = Mastodon.API.Search.Query(q: viewModel.searchText.value, + type: viewModel.searchScope.value, + accountID: nil, maxID: nil, minID: nil, - type: viewModel.searchScope.value, excludeUnreviewed: nil, - q: viewModel.searchText.value, resolve: nil, limit: nil, offset: offset, following: nil) + viewModel.context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox) .sink { completion in switch completion { @@ -81,7 +82,7 @@ extension SearchViewModel.LoadOldestState { } } receiveValue: { result in switch viewModel.searchScope.value { - case Mastodon.API.Search.Scope.accounts.rawValue: + case Mastodon.API.Search.SearchType.accounts: if result.value.accounts.isEmpty { stateMachine.enter(NoMore.self) } else { @@ -91,7 +92,7 @@ extension SearchViewModel.LoadOldestState { viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: newAccounts.removeDuplicate(), statuses: oldSearchResult.statuses, hashtags: oldSearchResult.hashtags) stateMachine.enter(Idle.self) } - case Mastodon.API.Search.Scope.hashtags.rawValue: + case Mastodon.API.Search.SearchType.hashtags: if result.value.hashtags.isEmpty { stateMachine.enter(NoMore.self) } else { diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index df13ac48c..06b654b3d 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -22,7 +22,7 @@ final class SearchViewModel: NSObject { // output let searchText = CurrentValueSubject("") - let searchScope = CurrentValueSubject("") + let searchScope = CurrentValueSubject(Mastodon.API.Search.SearchType.default) let isSearching = CurrentValueSubject(false) @@ -68,12 +68,12 @@ final class SearchViewModel: NSObject { } .flatMap { (text, scope) -> AnyPublisher, Error> in - let query = Mastodon.API.Search.Query(accountID: nil, + let query = Mastodon.API.Search.Query(q: text, + type: scope, + accountID: nil, maxID: nil, minID: nil, - type: scope, excludeUnreviewed: nil, - q: text, resolve: nil, limit: nil, offset: nil, @@ -101,25 +101,27 @@ final class SearchViewModel: NSObject { searchScope ) .filter { isSearching, text, _ in - isSearching && text.isEmpty + isSearching } - .sink { [weak self] _, _, scope in + .sink { [weak self] _, text, scope in guard let self = self else { return } guard let searchHistories = self.fetchSearchHistory() else { return } guard let dataSource = self.searchResultDiffableDataSource else { return } var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.mixed]) - - searchHistories.forEach { searchHistory in - let containsAccount = scope == Mastodon.API.Search.Scope.accounts.rawValue || scope == "" - let containsHashTag = scope == Mastodon.API.Search.Scope.hashtags.rawValue || scope == "" - 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) + if text.isEmpty { + 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) @@ -166,7 +168,7 @@ final class SearchViewModel: NSObject { snapshot.appendSections([.account]) let items = accounts.compactMap { SearchResultItem.account(account: $0) } snapshot.appendItems(items, toSection: .account) - if self.searchScope.value == Mastodon.API.Search.Scope.accounts.rawValue && !items.isEmpty { + if self.searchScope.value == Mastodon.API.Search.SearchType.accounts && !items.isEmpty { snapshot.appendItems([.bottomLoader], toSection: .account) } } @@ -174,7 +176,7 @@ final class SearchViewModel: NSObject { snapshot.appendSections([.hashtag]) let items = tags.compactMap { SearchResultItem.hashtag(tag: $0) } snapshot.appendItems(items, toSection: .hashtag) - if self.searchScope.value == Mastodon.API.Search.Scope.hashtags.rawValue && !items.isEmpty { + if self.searchScope.value == Mastodon.API.Search.SearchType.hashtags && !items.isEmpty { snapshot.appendItems([.bottomLoader], toSection: .hashtag) } } diff --git a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift b/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift index dcd4c4971..7ab18bb0c 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift @@ -40,7 +40,7 @@ final class SearchBottomLoader: UITableViewCell { func _init() { selectionStyle = .none - backgroundColor = Asset.Colors.lightWhite.color + backgroundColor = Asset.Colors.Background.systemGroupedBackground.color contentView.addSubview(activityIndicatorView) activityIndicatorView.constrainToCenter() } diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index cdfcdce23..aab8a5706 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -20,7 +20,7 @@ final class SearchingTableViewCell: UITableViewCell { let _titleLabel: UILabel = { let label = UILabel() - label.textColor = Asset.Colors.buttonDefault.color + label.textColor = Asset.Colors.brandBlue.color label.font = .systemFont(ofSize: 17, weight: .semibold) label.lineBreakMode = .byTruncatingTail return label diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index df876a635..216f18f98 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -28,7 +28,7 @@ class SearchRecommendCollectionHeader: UIView { let seeAllButton: UIButton = { let button = UIButton(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 }() diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift index dc6ef71e7..be8bb2607 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift @@ -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, @@ -109,9 +106,11 @@ extension Mastodon.API.Search { } public extension Mastodon.API.Search { - enum Scope: String { + enum SearchType: String, Codable { case accounts case hashtags + case statuses + case `default` public var rawValue: String { switch self { @@ -119,6 +118,10 @@ public extension Mastodon.API.Search { return "accounts" case .hashtags: return "hashtags" + case .statuses: + return "statuses" + case .default: + return "" } } } From 0dab9acd91bbab0b1bf4352e26adb9e325eb6429 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 12:22:05 +0800 Subject: [PATCH 06/20] fix: tag and searchHistory repeated save in CoreDate --- .../CoreData.xcdatamodel/contents | 32 ++++----- CoreDataStack/Entity/History.swift | 20 ++++++ CoreDataStack/Entity/SearchHistory.swift | 18 ++++- CoreDataStack/Entity/Tag.swift | 60 +++++++++++++---- Mastodon.xcodeproj/project.pbxproj | 8 +-- Mastodon/Extension/Array.swift | 20 ------ ...earchRecommendTagsCollectionViewCell.swift | 3 +- .../SearchViewModel+LoadOldestState.swift | 6 +- Mastodon/Scene/Search/SearchViewModel.swift | 53 +++++++++++++-- .../SearchingTableViewCell.swift | 4 +- .../CoreData/APIService+CoreData+Tag.swift | 66 +++++++++++++++++++ 11 files changed, 225 insertions(+), 65 deletions(-) delete mode 100644 Mastodon/Extension/Array.swift create mode 100644 Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 24a3955e7..f635a3db0 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -149,6 +149,7 @@ + @@ -194,24 +195,25 @@ + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/CoreDataStack/Entity/History.swift b/CoreDataStack/Entity/History.swift index 114879298..6fe703e84 100644 --- a/CoreDataStack/Entity/History.swift +++ b/CoreDataStack/Entity/History.swift @@ -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 diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift index 33b8a6010..d924917ee 100644 --- a/CoreDataStack/Entity/SearchHistory.swift +++ b/CoreDataStack/Entity/SearchHistory.swift @@ -12,6 +12,7 @@ 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? @@ -22,6 +23,13 @@ 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 @@ -31,7 +39,6 @@ extension SearchHistory { ) -> SearchHistory { let searchHistory: SearchHistory = context.insertObject() searchHistory.account = account - searchHistory.createAt = Date() return searchHistory } @@ -42,13 +49,18 @@ extension SearchHistory { ) -> SearchHistory { let searchHistory: SearchHistory = context.insertObject() searchHistory.hashtag = hashtag - searchHistory.createAt = Date() 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.createAt, ascending: false)] + return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)] } } diff --git a/CoreDataStack/Entity/Tag.swift b/CoreDataStack/Entity/Tag.swift index 2f1914c4a..3044cacc0 100644 --- a/CoreDataStack/Entity/Tag.swift +++ b/CoreDataStack/Entity/Tag.swift @@ -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? - + // one-to-many relationship @NSManaged public private(set) var histories: Set? } -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) } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 97b65eeb1..d002ccd1a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -30,7 +30,6 @@ 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 */; }; - 2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A0A261D5A5600B44727 /* Array.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 */; }; @@ -92,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 */; }; @@ -401,7 +401,6 @@ 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = ""; }; 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; - 2D0B7A0A261D5A5600B44727 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; @@ -460,6 +459,7 @@ 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; 2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = ""; }; 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = ""; }; 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = ""; }; 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = ""; }; @@ -1321,6 +1321,7 @@ 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.swift */, DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */, DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */, + 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */, ); path = CoreData; sourceTree = ""; @@ -1550,7 +1551,6 @@ 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, - 2D0B7A0A261D5A5600B44727 /* Array.swift */, DB68A06225E905E000CFDF14 /* UIApplication.swift */, DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */, 2D42FF8E25C8228A004A627A /* UIButton.swift */, @@ -2207,6 +2207,7 @@ 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, + 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */, @@ -2284,7 +2285,6 @@ 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, - 2D0B7A0B261D5A5600B44727 /* Array.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, diff --git a/Mastodon/Extension/Array.swift b/Mastodon/Extension/Array.swift deleted file mode 100644 index 91c2e3d66..000000000 --- a/Mastodon/Extension/Array.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Array.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/7. -// - -import Foundation - -public extension Array where Element: Equatable { - - func removeDuplicate() -> Array { - return self.enumerated().filter { (index,value) -> Bool in - return self.firstIndex(of: value) == index - }.map { (_, value) in - value - } - } -} - diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 08090f804..f7ff5f33e 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -82,7 +82,8 @@ extension SearchRecommendTagsCollectionViewCell { peopleLabel.text = "" return } - let recentHistory = historys[0...2] + + 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 diff --git a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift index 7088f1360..c76ab202c 100644 --- a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift @@ -89,7 +89,8 @@ extension SearchViewModel.LoadOldestState { var newAccounts = [Mastodon.Entity.Account]() newAccounts.append(contentsOf: oldSearchResult.accounts) newAccounts.append(contentsOf: result.value.accounts) - viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: newAccounts.removeDuplicate(), statuses: oldSearchResult.statuses, hashtags: oldSearchResult.hashtags) + 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: @@ -99,7 +100,8 @@ extension SearchViewModel.LoadOldestState { var newTags = [Mastodon.Entity.Tag]() newTags.append(contentsOf: oldSearchResult.hashtags) newTags.append(contentsOf: result.value.hashtags) - viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: oldSearchResult.accounts, statuses: oldSearchResult.statuses, hashtags: newTags.removeDuplicate()) + newTags.removeDuplicates() + viewModel.searchResult.value = Mastodon.Entity.SearchResult(accounts: oldSearchResult.accounts, statuses: oldSearchResult.statuses, hashtags: newTags) stateMachine.enter(Idle.self) } default: diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 06b654b3d..18954665c 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -234,6 +234,7 @@ final class SearchViewModel: NSObject { } func saveItemToCoreData(item: SearchResultItem) { + let searchHistories = self.fetchSearchHistory() _ = context.managedObjectContext.performChanges { [weak self] in guard let self = self else { return } switch item { @@ -255,15 +256,55 @@ final class SearchViewModel: NSObject { } }() let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.context.managedObjectContext, for: requestMastodonUser, in: activeMastodonAuthenticationBox.domain, entity: account, userCache: nil, networkDate: Date(), log: OSLog.api) - SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser) + 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) + } case .hashtag(let tag): - let histories = tag.history?[0 ... 2].compactMap { history -> History in - History.insert(into: self.context.managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) + 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) + } + 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()) + } + } + 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 = Tag.insert(into: self.context.managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) - SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData) - default: break } diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index aab8a5706..9fe0f1336 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -96,7 +96,7 @@ extension SearchingTableViewCell { _subTitleLabel.text = "" return } - let recentHistory = historys[0 ... 2] + 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 @@ -112,7 +112,7 @@ extension SearchingTableViewCell { _subTitleLabel.text = "" return } - let recentHistory = historys[0 ... 2] + 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 diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift new file mode 100644 index 000000000..3f931ddea --- /dev/null +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift @@ -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.. Date: Thu, 8 Apr 2021 12:31:48 +0800 Subject: [PATCH 07/20] chore: add navigation to hashtagViewController --- .../Scene/Search/SearchViewController+Searching.swift | 2 +- Mastodon/Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 34acf443f..540fd20c3 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -86,6 +86,6 @@ extension SearchViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - viewModel.saveItemToCoreData(item: item) + viewModel.searchResultItemDidSelected(item: item, from: self) } } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 470c88e0d..d506ab993 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -15,7 +15,7 @@ final class SearchViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = SearchViewModel(context: context) + private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator) let searchBar: UISearchBar = { let searchBar = UISearchBar() diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 18954665c..054ac8ce3 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -19,6 +19,7 @@ final class SearchViewModel: NSObject { // input let context: AppContext + weak var coordinator: SceneCoordinator! // output let searchText = CurrentValueSubject("") @@ -51,7 +52,8 @@ final class SearchViewModel: NSObject { lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) - init(context: AppContext) { + init(context: AppContext,coordinator: SceneCoordinator) { + self.coordinator = coordinator self.context = context super.init() @@ -233,7 +235,7 @@ final class SearchViewModel: NSObject { } } - func saveItemToCoreData(item: SearchResultItem) { + func searchResultItemDidSelected(item: SearchResultItem,from: UIViewController) { let searchHistories = self.fetchSearchHistory() _ = context.managedObjectContext.performChanges { [weak self] in guard let self = self else { return } @@ -285,6 +287,8 @@ final class SearchViewModel: NSObject { } else { SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData) } + let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) + 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 @@ -305,6 +309,9 @@ final class SearchViewModel: NSObject { history.update(updatedAt: Date()) } } + let tagInCoreData = self.context.managedObjectContext.object(with: hashtagObjectID) as! Tag + let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) + self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) default: break } From 803ff3a7fd3e6bcadb682c675e1b268ea83b8492 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 12:47:13 +0800 Subject: [PATCH 08/20] chore: add navigation to profile, add recommend navigation --- .../SearchViewController+Recomend.swift | 14 +++++ Mastodon/Scene/Search/SearchViewModel.swift | 52 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Search/SearchViewController+Recomend.swift b/Mastodon/Scene/Search/SearchViewController+Recomend.swift index 87f32261c..5eacd2d80 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recomend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recomend.swift @@ -5,6 +5,8 @@ // Created by sxiaojian on 2021/3/31. // +import CoreData +import CoreDataStack import Foundation import MastodonSDK import OSLog @@ -54,6 +56,18 @@ 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 account = diffableDataSource.itemIdentifier(for: indexPath) else { return } + viewModel.accountCollectionViewItemDidSelected(account: account, 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 + } } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 054ac8ce3..46255fde1 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -235,6 +235,41 @@ final class SearchViewModel: NSObject { } } + func accountCollectionViewItemDidSelected(account: Mastodon.Entity.Account, from: UIViewController) { + _ = context.managedObjectContext.performChanges { [weak self] in + guard let self = self else { return } + 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) + let viewModel = ProfileViewModel(context: self.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: self.context.managedObjectContext, entity: hashtag) + let viewModel = HashtagTimelineViewModel(context: self.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 = self.fetchSearchHistory() _ = context.managedObjectContext.performChanges { [weak self] in @@ -271,6 +306,10 @@ final class SearchViewModel: NSObject { } 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) @@ -288,7 +327,9 @@ final class SearchViewModel: NSObject { SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData) } let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) - self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) + 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 @@ -299,6 +340,11 @@ final class SearchViewModel: NSObject { 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 @@ -311,7 +357,9 @@ final class SearchViewModel: NSObject { } let tagInCoreData = self.context.managedObjectContext.object(with: hashtagObjectID) as! Tag let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) - self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) + DispatchQueue.main.async { + self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) + } default: break } From 4ffea58f711ba0d8c419e44d6169dfb2662c9038 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 13:30:32 +0800 Subject: [PATCH 09/20] fix: SearchCard UI update, change follow button border width, add card border when DarkMode, add shadow --- Mastodon/Generated/Assets.swift | 6 +++ .../Colors/Border/Contents.json | 9 +++++ .../Border/searchCard.colorset/Contents.json | 38 +++++++++++++++++++ .../Colors/Shadow/Contents.json | 9 +++++ .../Shadow/SearchCard.colorset/Contents.json | 38 +++++++++++++++++++ ...hRecommendAccountsCollectionViewCell.swift | 31 +++++++++++---- ...earchRecommendTagsCollectionViewCell.swift | 16 ++++++-- 7 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Border/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Border/searchCard.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Shadow/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Shadow/SearchCard.colorset/Contents.json diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index c8fccbfef..eb69bda10 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -51,6 +51,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 +69,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") } diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Border/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Border/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Border/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Border/searchCard.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Border/searchCard.colorset/Contents.json new file mode 100644 index 000000000..a0ce2efb8 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Border/searchCard.colorset/Contents.json @@ -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 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Shadow/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Shadow/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Shadow/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Shadow/SearchCard.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Shadow/SearchCard.colorset/Contents.json new file mode 100644 index 000000000..a28cf0793 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Shadow/SearchCard.colorset/Contents.json @@ -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 + } +} diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index b4305eeff..b6eafb3f9 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -12,18 +12,23 @@ import UIKit class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { 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.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 @@ -46,7 +51,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { 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.borderWidth = 2 button.layer.borderColor = UIColor.white.cgColor return button }() @@ -55,6 +60,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { super.prepareForReuse() headerImageView.af.cancelImageRequest() avatarImageView.af.cancelImageRequest() + visualEffectView.removeFromSuperview() } override init(frame: CGRect) { @@ -69,11 +75,17 @@ 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 + 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) @@ -115,8 +127,11 @@ extension SearchRecommendAccountsCollectionViewCell { headerImageView.af.setImage( withURL: URL(string: account.header)!, placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) + 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) + } } } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index f7ff5f33e..813c8a34f 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -15,7 +15,7 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - + let hashtagTitleLabel: UILabel = { let label = UILabel() label.textColor = .white @@ -58,10 +58,20 @@ 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 + 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) From cc4290385d8c57c8a07195d982e902c4799b977b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 14:01:22 +0800 Subject: [PATCH 10/20] chore: rename recommend --- Mastodon.xcodeproj/project.pbxproj | 8 ++++---- ...ecomend.swift => SearchViewController+Recommend.swift} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename Mastodon/Scene/Search/{SearchViewController+Recomend.swift => SearchViewController+Recommend.swift} (99%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d002ccd1a..01d5f1f20 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -45,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+Recomend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.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 */; }; @@ -416,7 +416,7 @@ 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; - 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Recomend.swift"; sourceTree = ""; }; + 2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Recommend.swift"; sourceTree = ""; }; 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = ""; }; 2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = ""; }; 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = ""; }; @@ -1611,7 +1611,7 @@ 2DFAD5212616F8E300F9EE7C /* TableViewCell */, 2DE0FAC62615F5D200CDF649 /* View */, DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, - 2D34D9CA261489930081BFC0 /* SearchViewController+Recomend.swift */, + 2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */, 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */, @@ -2311,7 +2311,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, - 2D34D9CB261489930081BFC0 /* SearchViewController+Recomend.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 */, diff --git a/Mastodon/Scene/Search/SearchViewController+Recomend.swift b/Mastodon/Scene/Search/SearchViewController+Recommend.swift similarity index 99% rename from Mastodon/Scene/Search/SearchViewController+Recomend.swift rename to Mastodon/Scene/Search/SearchViewController+Recommend.swift index 5eacd2d80..056df2e38 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recomend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recommend.swift @@ -1,5 +1,5 @@ // -// SearchViewController+Recomend.swift +// SearchViewController+Recommend.swift // Mastodon // // Created by sxiaojian on 2021/3/31. From c1971438cdf0a493b0d5619d09d1ab87f25a6867 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 14:17:57 +0800 Subject: [PATCH 11/20] fix: acctLabel display beyound card --- .../SearchRecommendAccountsCollectionViewCell.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index b6eafb3f9..cda517f43 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -41,6 +41,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let label = UILabel() label.textColor = .white label.font = .preferredFont(forTextStyle: .body) + label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label }() @@ -105,6 +106,8 @@ extension SearchRecommendAccountsCollectionViewCell { 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) ]) From 5c7a13e6b31aa295bad03942f6779031b3a6dd23 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 14:19:59 +0800 Subject: [PATCH 12/20] fix: displayNameLabel display beyound card --- .../SearchRecommendAccountsCollectionViewCell.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index cda517f43..626a2b4b6 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -32,6 +32,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let displayNameLabel: UILabel = { let label = UILabel() label.textColor = .white + label.textAlignment = .center label.font = .systemFont(ofSize: 18, weight: .semibold) label.translatesAutoresizingMaskIntoConstraints = false return label @@ -100,6 +101,8 @@ 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) ]) From ae20a290136b8b250d684c3f92a04f1213add37f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 8 Apr 2021 16:12:04 +0800 Subject: [PATCH 13/20] fix: make seeAll button and clear button highlight when user tapping --- Mastodon/Scene/Search/SearchViewController.swift | 4 ++-- .../Scene/Search/View/SearchRecommendCollectionHeader.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index d506ab993..b98a34b95 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -99,8 +99,8 @@ final class SearchViewController: UIViewController, NeedsDependency { return label }() - let clearSearchHistoryButton: UIButton = { - let button = UIButton(type: .custom) + let clearSearchHistoryButton: HighlightDimmableButton = { + let button = HighlightDimmableButton(type: .custom) button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal) return button diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index 216f18f98..bc5bd7663 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -25,8 +25,8 @@ 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) return button From 0418ec147021f2f3d1fa2646814a8323b7bd5ffa Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 13:09:30 +0800 Subject: [PATCH 14/20] chore: recommend account use CoreData dateSource --- .../Section/RecommendAccountSection.swift | 10 ++- ...hRecommendAccountsCollectionViewCell.swift | 3 +- .../SearchViewController+Recommend.swift | 5 +- .../Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 70 +++++++++---------- .../APIService/APIService+Recommend.swift | 31 ++++++-- 6 files changed, 73 insertions(+), 48 deletions(-) diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift index b08c9abab..ac3feb328 100644 --- a/Mastodon/Diffiable/Section/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -8,6 +8,8 @@ import Foundation import MastodonSDK import UIKit +import CoreData +import CoreDataStack enum RecommendAccountSection: Equatable, Hashable { case main @@ -15,10 +17,12 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( - for collectionView: UICollectionView - ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, account -> UICollectionViewCell? in + for collectionView: UICollectionView, + managedObjectContext: NSManagedObjectContext + ) -> UICollectionViewDiffableDataSource { + UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell + let account = managedObjectContext.object(with: objectID) as! MastodonUser cell.config(with: account) return cell } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 626a2b4b6..4380d98f9 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -8,6 +8,7 @@ import Foundation import MastodonSDK import UIKit +import CoreDataStack class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let avatarImageView: UIImageView = { @@ -122,7 +123,7 @@ extension SearchRecommendAccountsCollectionViewCell { ]) } - func config(with account: Mastodon.Entity.Account) { + func config(with account: MastodonUser) { displayNameLabel.text = account.displayName.isEmpty ? account.username : account.displayName acctLabel.text = account.acct avatarImageView.af.setImage( diff --git a/Mastodon/Scene/Search/SearchViewController+Recommend.swift b/Mastodon/Scene/Search/SearchViewController+Recommend.swift index 056df2e38..e941fa841 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recommend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recommend.swift @@ -59,8 +59,9 @@ extension SearchViewController: UICollectionViewDelegate { switch collectionView { case self.accountsCollectionView: guard let diffableDataSource = viewModel.accountDiffableDataSource else { return } - guard let account = diffableDataSource.itemIdentifier(for: indexPath) else { return } - viewModel.accountCollectionViewItemDidSelected(account: account, from: self) + 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 } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index b98a34b95..f697ef528 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -150,7 +150,7 @@ extension SearchViewController { func setupDataSource() { viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) - viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, managedObjectContext: context.managedObjectContext) viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 46255fde1..c3a5987ba 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -30,10 +30,10 @@ final class SearchViewModel: NSObject { let searchResult = CurrentValueSubject(nil) var recommendHashTags = [Mastodon.Entity.Tag]() - var recommendAccounts = [Mastodon.Entity.Account]() + var recommendAccounts = [NSManagedObjectID]() var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? - var accountDiffableDataSource: UICollectionViewDiffableDataSource? + var accountDiffableDataSource: UICollectionViewDiffableDataSource? var searchResultDiffableDataSource: UITableViewDiffableDataSource? // bottom loader @@ -52,7 +52,7 @@ final class SearchViewModel: NSObject { lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) - init(context: AppContext,coordinator: SceneCoordinator) { + init(context: AppContext, coordinator: SceneCoordinator) { self.coordinator = coordinator self.context = context super.init() @@ -102,7 +102,7 @@ final class SearchViewModel: NSObject { searchText, searchScope ) - .filter { isSearching, text, _ in + .filter { isSearching, _, _ in isSearching } .sink { [weak self] _, text, scope in @@ -151,7 +151,7 @@ final class SearchViewModel: NSObject { guard let self = self else { return } if !self.recommendAccounts.isEmpty { guard let dataSource = self.accountDiffableDataSource else { return } - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(self.recommendAccounts, toSection: .main) dataSource.apply(snapshot, animatingDifferences: false, completion: nil) @@ -170,7 +170,7 @@ final class SearchViewModel: NSObject { 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 { + if self.searchScope.value == Mastodon.API.Search.SearchType.accounts, !items.isEmpty { snapshot.appendItems([.bottomLoader], toSection: .account) } } @@ -178,7 +178,7 @@ final class SearchViewModel: NSObject { 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 { + if self.searchScope.value == Mastodon.API.Search.SearchType.hashtags, !items.isEmpty { snapshot.appendItems([.bottomLoader], toSection: .hashtag) } } @@ -229,49 +229,45 @@ final class SearchViewModel: NSObject { } } 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(account: Mastodon.Entity.Account, from: UIViewController) { - _ = context.managedObjectContext.performChanges { [weak self] in - guard let self = self else { return } - 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) - let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: mastodonUser) - DispatchQueue.main.async { - self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show) - } + 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: self.context.managedObjectContext, entity: hashtag) - let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) + 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 = self.fetchSearchHistory() + func searchResultItemDidSelected(item: SearchResultItem, from: UIViewController) { + let searchHistories = fetchSearchHistory() _ = context.managedObjectContext.performChanges { [weak self] in guard let self = self else { return } switch item { @@ -312,7 +308,7 @@ final class SearchViewModel: NSObject { } case .hashtag(let tag): - let (tagInCoreData,_) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: 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 } diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/Mastodon/Service/APIService/APIService+Recommend.swift index bf6db0179..1c58fc575 100644 --- a/Mastodon/Service/APIService/APIService+Recommend.swift +++ b/Mastodon/Service/APIService/APIService+Recommend.swift @@ -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, 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, Error> { - return Mastodon.API.Trends.get(session: session, domain: domain, query: query) + Mastodon.API.Trends.get(session: session, domain: domain, query: query) } } From c74314ef11d5fa075fdeb7e7bd80cd7f0b8530cd Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 13:59:33 +0800 Subject: [PATCH 15/20] chore: observe Follow state --- .../Section/RecommendAccountSection.swift | 9 ++-- ...hRecommendAccountsCollectionViewCell.swift | 52 +++++++++++++++++-- .../Scene/Search/SearchViewController.swift | 2 +- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift index ac3feb328..409adee3e 100644 --- a/Mastodon/Diffiable/Section/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -18,12 +18,15 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( for collectionView: UICollectionView, - managedObjectContext: NSManagedObjectContext + context: AppContext! ) -> UICollectionViewDiffableDataSource { UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell - let account = managedObjectContext.object(with: objectID) as! MastodonUser - cell.config(with: account) + let user = context.managedObjectContext.object(with: objectID) as! MastodonUser + cell.config(with: user) + if let currentUser = context.authenticationService.activeMastodonAuthentication.value?.user { + cell.configFollowButton(with: user, currentMastodonUser: currentUser) + } return cell } } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 4380d98f9..933eeb42a 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -9,8 +9,12 @@ import Foundation import MastodonSDK import UIKit import CoreDataStack +import Combine class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { + + var disposeBag = Set() + let avatarImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 8.4 @@ -123,16 +127,16 @@ extension SearchRecommendAccountsCollectionViewCell { ]) } - func config(with account: MastodonUser) { - 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 } @@ -140,6 +144,44 @@ extension SearchRecommendAccountsCollectionViewCell { self.visualEffectView.pin(top: 0, left: 0, bottom: 0, right: 0) } } + + func configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) { + self._configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser) + 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) + } + .store(in: &disposeBag) + } + + func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) { + 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) + } + self.followButton.setTitle(relationshipActionSet.title, for: .normal) + } } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index f697ef528..11a5630f3 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -150,7 +150,7 @@ extension SearchViewController { func setupDataSource() { viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) - viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, managedObjectContext: context.managedObjectContext) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, context: context) viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } From 567c2af0eea50833e15a003b19f4bd68e72eddeb Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 19:39:35 +0800 Subject: [PATCH 16/20] chore: add followAction --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Section/RecommendAccountSection.swift | 15 +- ...hRecommendAccountsCollectionViewCell.swift | 60 +++----- .../Search/SearchViewController+Follow.swift | 137 ++++++++++++++++++ .../Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 16 ++ 6 files changed, 185 insertions(+), 49 deletions(-) create mode 100644 Mastodon/Scene/Search/SearchViewController+Follow.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 01d5f1f20..fda5de623 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -135,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 */; }; @@ -506,6 +507,7 @@ 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = ""; }; 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = ""; }; 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; + 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1613,6 +1615,7 @@ DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, 2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */, 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */, + 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */, 2D34D9E026149C550081BFC0 /* CollectionViewCell */, @@ -2355,6 +2358,7 @@ 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 */, diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift index 409adee3e..3ecd4e3b2 100644 --- a/Mastodon/Diffiable/Section/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -5,11 +5,11 @@ // Created by sxiaojian on 2021/4/1. // +import CoreData +import CoreDataStack import Foundation import MastodonSDK import UIKit -import CoreData -import CoreDataStack enum RecommendAccountSection: Equatable, Hashable { case main @@ -18,15 +18,14 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( for collectionView: UICollectionView, - context: AppContext! + delegate: SearchRecommendAccountsCollectionViewCellDelegate, + managedObjectContext: NSManagedObjectContext ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, objectID -> UICollectionViewCell? in + UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell - let user = context.managedObjectContext.object(with: objectID) as! MastodonUser + let user = managedObjectContext.object(with: objectID) as! MastodonUser + cell.delegate = delegate cell.config(with: user) - if let currentUser = context.authenticationService.activeMastodonAuthentication.value?.user { - cell.configFollowButton(with: user, currentMastodonUser: currentUser) - } return cell } } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 933eeb42a..c64db4981 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -5,16 +5,23 @@ // Created by sxiaojian on 2021/4/1. // +import Combine +import CoreDataStack import Foundation import MastodonSDK import UIKit -import CoreDataStack -import Combine + +protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { + func followButtonDidPressed(clickedUser: MastodonUser) + + func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) +} class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { - var disposeBag = Set() + weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate? + let avatarImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 8.4 @@ -52,8 +59,8 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { 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) @@ -138,49 +145,22 @@ extension SearchRecommendAccountsCollectionViewCell { headerImageView.af.setImage( withURL: URL(string: mastodonUser.header)!, placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2)) { [weak self] _ in + 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) } - } - - func configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) { - self._configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser) - 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) + delegate?.configFollowButton(with: mastodonUser, followButton: followButton) + followButton.publisher(for: .touchUpInside) + .sink { [weak self] _ in + self?.followButtonDidPressed(mastodonUser: mastodonUser) } .store(in: &disposeBag) } - func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) { - 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) - } - self.followButton.setTitle(relationshipActionSet.title, for: .normal) + func followButtonDidPressed(mastodonUser: MastodonUser) { + delegate?.followButtonDidPressed(clickedUser: mastodonUser) } } diff --git a/Mastodon/Scene/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/SearchViewController+Follow.swift new file mode 100644 index 000000000..ce7d13b8a --- /dev/null +++ b/Mastodon/Scene/Search/SearchViewController+Follow.swift @@ -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 { + 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 + } + let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser) + 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 + } +} diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 11a5630f3..f3e2e0f0d 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -150,7 +150,7 @@ extension SearchViewController { func setupDataSource() { viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) - viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, context: context) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, delegate: self, managedObjectContext: context.managedObjectContext) viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index c3a5987ba..3313d1760 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -21,6 +21,9 @@ final class SearchViewModel: NSObject { let context: AppContext weak var coordinator: SceneCoordinator! + let mastodonUser = CurrentValueSubject(nil) + let currentMastodonUser = CurrentValueSubject(nil) + // output let searchText = CurrentValueSubject("") let searchScope = CurrentValueSubject(Mastodon.API.Search.SearchType.default) @@ -60,6 +63,19 @@ final class SearchViewModel: NSObject { 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(), From aa23ce398f7ccf676e2295dbd3e17cbda9cd0b1f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 20:04:12 +0800 Subject: [PATCH 17/20] fix: fix crash when unfollowing , fix cell reuse issue --- .../SearchRecommendAccountsCollectionViewCell.swift | 1 + Mastodon/Scene/Search/SearchViewController+Follow.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index c64db4981..85c543b40 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -75,6 +75,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { headerImageView.af.cancelImageRequest() avatarImageView.af.cancelImageRequest() visualEffectView.removeFromSuperview() + disposeBag.removeAll() } override init(frame: CGRect) { diff --git a/Mastodon/Scene/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/SearchViewController+Follow.swift index ce7d13b8a..8b0acda0a 100644 --- a/Mastodon/Scene/Search/SearchViewController+Follow.swift +++ b/Mastodon/Scene/Search/SearchViewController+Follow.swift @@ -24,7 +24,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat guard let currentMastodonUser = viewModel.currentMastodonUser.value else { return } - let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser) + guard let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) else { return } switch relationshipAction { case .none: break From a007b7a98005b2ec275bbb93ba4cc32d45681b7b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 20:38:02 +0800 Subject: [PATCH 18/20] fix: color in Light Mode, fix search result disappear when push new page --- Mastodon/Generated/Assets.swift | 1 + .../searchResult.colorset/Contents.json | 38 +++++++++++++++++++ .../Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 28 +++++++------- 4 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index eb69bda10..b6cdde9c5 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -44,6 +44,7 @@ internal enum Asset { 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 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") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json new file mode 100644 index 000000000..3338422aa --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json @@ -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 + } +} diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index b98a34b95..ee6b91903 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -77,7 +77,7 @@ final class SearchViewController: UIViewController, NeedsDependency { // searching let searchingTableView: UITableView = { let tableView = UITableView() - tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + 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) diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 46255fde1..2bd5b14df 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -106,27 +106,27 @@ final class SearchViewModel: NSObject { 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() - if text.isEmpty { - 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) - } + 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) From e784e123c9a3512eb3f1729dfa4aad517a7c4e11 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 12 Apr 2021 12:49:01 +0800 Subject: [PATCH 19/20] fix: searchbar wasn't correct display in ipad --- .../SearchViewController+Searching.swift | 2 +- .../Scene/Search/SearchViewController.swift | 34 +++++++++++++++---- .../SearchingTableViewCell.swift | 2 ++ ...veStatusBarStyleNavigationController.swift | 26 +++++++++++++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 540fd20c3..3eb9793ad 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -20,7 +20,7 @@ extension SearchViewController { searchingTableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) view.addSubview(searchingTableView) searchingTableView.constrain([ - searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + 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), diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index ee6b91903..33caabd3a 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -17,6 +17,12 @@ final class SearchViewController: UIViewController, NeedsDependency { var disposeBag = Set() 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() searchBar.placeholder = L10n.Scene.Search.Searchbar.placeholder @@ -25,7 +31,6 @@ final class SearchViewController: UIViewController, NeedsDependency { let micImage = UIImage(systemName: "mic.fill") searchBar.setImage(micImage, for: .bookmark, state: .normal) searchBar.showsBookmarkButton = true - searchBar.showsScopeBar = false 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 @@ -110,16 +115,15 @@ final class SearchViewController: UIViewController, NeedsDependency { extension SearchViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color let barAppearance = UINavigationBarAppearance() barAppearance.configureWithTransparentBackground() - barAppearance.backgroundColor = Asset.Colors.Background.navigationBar.color navigationItem.standardAppearance = barAppearance navigationItem.compactAppearance = barAppearance navigationItem.scrollEdgeAppearance = barAppearance - searchBar.delegate = self - navigationItem.titleView = searchBar + view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color navigationItem.hidesBackButton = true + + setupSearchBar() setupScrollView() setupHashTagCollectionView() setupAccountsCollectionView() @@ -128,10 +132,28 @@ extension SearchViewController { 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), diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index 9fe0f1336..5a258d8a5 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -15,6 +15,8 @@ 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 }() diff --git a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift index 8ba7c2257..5f4302712 100644 --- a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift +++ b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift @@ -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) + } } } From 7bf6328252bf72300aa0d1fb8d48142b1099725c Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 13 Apr 2021 13:06:35 +0800 Subject: [PATCH 20/20] chore: code format and add layer.cornerCurve = .continuous --- .../SearchRecommendAccountsCollectionViewCell.swift | 3 +++ .../SearchRecommendTagsCollectionViewCell.swift | 1 + .../APIService/CoreData/APIService+CoreData+Tag.swift | 10 +++++----- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 85c543b40..f45124671 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -33,6 +33,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let imageView = UIImageView() 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 @@ -65,6 +66,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold) button.layer.cornerRadius = 12 + button.layer.cornerCurve = .continuous button.layer.borderWidth = 2 button.layer.borderColor = UIColor.white.cgColor return button @@ -99,6 +101,7 @@ extension SearchRecommendAccountsCollectionViewCell { private func configure() { headerImageView.backgroundColor = Asset.Colors.brandBlue.color 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) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 813c8a34f..d00cb0504 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -68,6 +68,7 @@ extension SearchRecommendTagsCollectionViewCell { private func configure() { backgroundColor = Asset.Colors.brandBlue.color layer.cornerRadius = 10 + layer.cornerCurve = .continuous clipsToBounds = false layer.borderWidth = 2 layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift index 3f931ddea..9b4319572 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Tag.swift @@ -28,7 +28,7 @@ extension APIService.CoreData { return nil } }() - + if let oldTag = oldTag { APIService.CoreData.merge(tag: oldTag, entity: entity, into: managedObjectContext) return (oldTag, false) @@ -40,8 +40,8 @@ extension APIService.CoreData { return (tagInCoreData, true) } } - - static func merge(tag:Tag,entity:Mastodon.Entity.Tag,into managedObjectContext: NSManagedObjectContext) { + + 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 } @@ -49,7 +49,7 @@ extension APIService.CoreData { if entityHistoriesCount == 0 { return } - for n in 0..