From 6e10efc490a30b5dd9420727b340403eb6c4261b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 2 Apr 2021 16:24:00 +0800 Subject: [PATCH] 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] } }