From 458ab6bcdaf1bf4bfd381d60d4cbbb9ad348fbe6 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 1 Apr 2021 20:54:57 +0800 Subject: [PATCH 1/4] feature: search recommend page --- Localization/app.json | 13 ++ Mastodon.xcodeproj/project.pbxproj | 24 +++ .../Section/RecomendHashTagSection.swift | 26 +++ .../Section/RecommendAccountSection.swift | 26 +++ Mastodon/Extension/UIView+Constraint.swift | 88 +++++----- Mastodon/Generated/Assets.swift | 1 + Mastodon/Generated/Strings.swift | 22 +++ .../Background/search.colorset/Contents.json | 20 +++ .../Resources/en.lproj/Localizable.strings | 7 + ...hRecommendAccountsCollectionViewCell.swift | 152 ++++++++++++++++++ ...earchRecommendTagsCollectionViewCell.swift | 54 ++++++- .../SearchViewController+recomendView.swift | 124 +++++++++++--- .../Scene/Search/SearchViewController.swift | 80 ++++++++- Mastodon/Scene/Search/SearchViewModel.swift | 73 ++++++--- .../SearchRecommendCollectionHeader.swift | 89 ++++++++++ .../Entity/Mastodon+Entity+Account.swift | 12 +- .../Entity/Mastodon+Entity+History.swift | 2 +- .../Entity/Mastodon+Entity+Tag.swift | 6 +- 18 files changed, 710 insertions(+), 109 deletions(-) create mode 100644 Mastodon/Diffiable/Section/RecomendHashTagSection.swift create mode 100644 Mastodon/Diffiable/Section/RecommendAccountSection.swift create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json create mode 100644 Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift create mode 100644 Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift diff --git a/Localization/app.json b/Localization/app.json index 94b785f2c..433515fef 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -241,6 +241,19 @@ "searchBar": { "placeholder": "Search hashtags and users", "cancel": "Cancel" + }, + "recommend": { + "buttonText": "See All", + "hash_tag": { + "title": "Trending in your timeline", + "description": "Hashtags that are getting quite a bit of attention among people you follow", + "people_talking": "%s people are talking" + }, + "accounts": { + "title": "Accounts you might like", + "description": "Except for Sam, you will not like his account.", + "follow": "Follow" + } } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 773c869b1..19228f2e7 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -94,6 +94,10 @@ 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; 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 */; }; + 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 */; }; 2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */; }; 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */; }; @@ -401,6 +405,10 @@ 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProviderFacade.swift; sourceTree = ""; }; 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewCellDelegate.swift"; sourceTree = ""; }; @@ -718,6 +726,7 @@ isa = PBXGroup; children = ( 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */, + 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */, ); path = CollectionViewCell; sourceTree = ""; @@ -871,6 +880,8 @@ DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, + 2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */, + 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, ); @@ -936,6 +947,14 @@ path = Decoration; sourceTree = ""; }; + 2DE0FAC62615F5D200CDF649 /* View */ = { + isa = PBXGroup; + children = ( + 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */, + ); + path = View; + sourceTree = ""; + }; 2DF75BB725D1473400694EC8 /* Stack */ = { isa = PBXGroup; children = ( @@ -1418,6 +1437,7 @@ DB9D6BEE25E4F5370051B173 /* Search */ = { isa = PBXGroup; children = ( + 2DE0FAC62615F5D200CDF649 /* View */, DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, @@ -1896,6 +1916,7 @@ DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */, DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, + 2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */, 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, @@ -1911,6 +1932,7 @@ 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */, + 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, @@ -1939,6 +1961,7 @@ DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */, 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */, + 2DE0FAC12615F04D00CDF649 /* RecomendHashTagSection.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */, @@ -1983,6 +2006,7 @@ DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, + 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/RecomendHashTagSection.swift b/Mastodon/Diffiable/Section/RecomendHashTagSection.swift new file mode 100644 index 000000000..2f78e73b9 --- /dev/null +++ b/Mastodon/Diffiable/Section/RecomendHashTagSection.swift @@ -0,0 +1,26 @@ +// +// RecomendHashTagSection.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/1. +// + +import Foundation +import MastodonSDK +import UIKit + +enum RecomendHashTagSection: Equatable, Hashable { + case main +} + +extension RecomendHashTagSection { + static func collectionViewDiffableDataSource( + for collectionView: UICollectionView + ) -> 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) + return cell + } + } +} diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift new file mode 100644 index 000000000..b08c9abab --- /dev/null +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -0,0 +1,26 @@ +// +// RecommendAccountSection.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/1. +// + +import Foundation +import MastodonSDK +import UIKit + +enum RecommendAccountSection: Equatable, Hashable { + case main +} + +extension RecommendAccountSection { + static func collectionViewDiffableDataSource( + for collectionView: UICollectionView + ) -> UICollectionViewDiffableDataSource { + UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, account -> UICollectionViewCell? in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell + cell.config(with: account) + return cell + } + } +} diff --git a/Mastodon/Extension/UIView+Constraint.swift b/Mastodon/Extension/UIView+Constraint.swift index 42d3bfd93..baa923ada 100644 --- a/Mastodon/Extension/UIView+Constraint.swift +++ b/Mastodon/Extension/UIView+Constraint.swift @@ -42,17 +42,17 @@ extension UIView { attribute: .top, multiplier: 1.0, constant: toSuperviewEdges?.top ?? 0.0), - NSLayoutConstraint(item: self, + NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, - toItem: view, + toItem: self, attribute: .trailing, multiplier: 1.0, constant: toSuperviewEdges?.right ?? 0.0), - NSLayoutConstraint(item: self, + NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, - toItem: view, + toItem: self, attribute: .bottom, multiplier: 1.0, constant: toSuperviewEdges?.bottom ?? 0.0) @@ -89,40 +89,6 @@ extension UIView { constant: constant) } - func constraint(toBottom: UIView, constant: CGFloat) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: toBottom, attribute: .bottom, multiplier: 1.0, constant: constant) - } - - func pinToBottom(to: UIView, height: CGFloat) { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.width, toView: to), - constraint(toBottom: to, constant: 0.0), - constraint(.height, constant: height) - ]) - } - - func constraint(toTop: UIView, constant: CGFloat) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: toTop, attribute: .top, multiplier: 1.0, constant: constant) - } - - func constraint(toTrailing: UIView, constant: CGFloat) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: toTrailing, attribute: .trailing, multiplier: 1.0, constant: constant) - } - - func constraint(toLeading: UIView, constant: CGFloat) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: toLeading, attribute: .leading, multiplier: 1.0, constant: constant) - } - func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat, topLayoutGuide: UILayoutSupport) { guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } translatesAutoresizingMaskIntoConstraints = false @@ -204,17 +170,6 @@ extension UIView { ]) } - func pinTo(viewAbove: UIView, padding: CGFloat = 0.0, height: CGFloat? = nil) { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.width, toView: viewAbove), - constraint(toBottom: viewAbove, constant: padding), - self.centerXAnchor.constraint(equalTo: viewAbove.centerXAnchor), - height != nil ? constraint(.height, constant: height!) : nil - ]) - } - func pin(toSize: CGSize) { guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } translatesAutoresizingMaskIntoConstraints = false @@ -223,6 +178,25 @@ extension UIView { heightAnchor.constraint(equalToConstant: toSize.height)]) } + func pin(top: CGFloat?,left: CGFloat?,bottom: CGFloat?, right: CGFloat?) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + var constraints = [NSLayoutConstraint]() + if let topConstant = top { + constraints.append(topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant)) + } + if let leftConstant = left { + constraints.append(leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: leftConstant)) + } + if let bottomConstant = bottom { + constraints.append(view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomConstant)) + } + if let rightConstant = right { + constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightConstant)) + } + constrain(constraints) + + } func pinTopLeft(padding: CGFloat) { guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } translatesAutoresizingMaskIntoConstraints = false @@ -231,6 +205,14 @@ extension UIView { topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) } + func pinTopLeft(top: CGFloat, left: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: left), + topAnchor.constraint(equalTo: view.topAnchor, constant: top)]) + } + func pinTopRight(padding: CGFloat) { guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } translatesAutoresizingMaskIntoConstraints = false @@ -238,6 +220,14 @@ extension UIView { view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: padding), topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) } + + func pinTopRight(top: CGFloat, right: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: right), + topAnchor.constraint(equalTo: view.topAnchor, constant: top)]) + } func pinTopLeft(toView: UIView, topPadding: CGFloat) { guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 82e7f8b1f..cbb5c2940 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -42,6 +42,7 @@ 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/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 1294c78e2..98f08ef06 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -370,6 +370,28 @@ internal enum L10n { } } internal enum Search { + internal enum Recommend { + /// See All + 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") + /// Follow + internal static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow") + /// Accounts you might like + internal static let title = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Title") + } + internal enum HashTag { + /// Hashtags that are getting quite a bit of attention among people you follow + internal static let description = L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.Description") + /// %@ people are talking + internal static func peopleTalking(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.PeopleTalking", String(describing: p1)) + } + /// Trending in your timeline + internal static let title = L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.Title") + } + } internal enum Searchbar { /// Cancel internal static let cancel = L10n.tr("Localizable", "Scene.Search.Searchbar.Cancel") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json new file mode 100644 index 000000000..838e44e44 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/search.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "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/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index e229d0f48..db808b56d 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -115,6 +115,13 @@ tap the link to confirm your account."; "Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; "Scene.Register.Input.Username.Placeholder" = "username"; "Scene.Register.Title" = "Tell us about you."; +"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.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.ServerPicker.Button.Category.All" = "All"; diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift new file mode 100644 index 000000000..bed61f228 --- /dev/null +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -0,0 +1,152 @@ +// +// SearchRecommendAccountsCollectionViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/1. +// + +import Foundation +import UIKit +import MastodonSDK + +class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { + let avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 8 + imageView.clipsToBounds = true + return imageView + }() + + let headerImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 8 + imageView.clipsToBounds = true + return imageView + }() + + let displayNameLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .systemFont(ofSize: 18, weight: .semibold) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let acctLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .preferredFont(forTextStyle: .body) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let followButton: UIButton = { + let button = UIButton(type: .custom) + button.setTitleColor(.white, for: .normal) + button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold) + button.layer.cornerRadius = 12 + button.layer.borderWidth = 3 + button.layer.borderColor = UIColor.white.cgColor + return button + }() + + override func prepareForReuse() { + super.prepareForReuse() + headerImageView.af.cancelImageRequest() + avatarImageView.af.cancelImageRequest() + } + + override init(frame: CGRect) { + super.init(frame: .zero) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +extension SearchRecommendAccountsCollectionViewCell { + private func configure() { + headerImageView.backgroundColor = Asset.Colors.buttonDefault.color + layer.cornerRadius = 8 + clipsToBounds = true + + contentView.addSubview(headerImageView) + headerImageView.pin(top: 16, left: 0, bottom: 0, right: 0 ) + + contentView.addSubview(avatarImageView) + avatarImageView.pin(toSize: CGSize(width: 88, height: 88)) + avatarImageView.constrain([ + avatarImageView.constraint(.top, toView: contentView), + avatarImageView.constraint(.centerX, toView: contentView) + ]) + + contentView.addSubview(displayNameLabel) + displayNameLabel.constrain([ + displayNameLabel.constraint(.top, toView: contentView,constant: 108), + displayNameLabel.constraint(.centerX, toView: contentView) + ]) + + contentView.addSubview(acctLabel) + acctLabel.constrain([ + acctLabel.constraint(.top, toView: contentView,constant: 132), + acctLabel.constraint(.centerX, toView: contentView) + ]) + + contentView.addSubview(followButton) + followButton.pin(toSize: CGSize(width: 76, height: 24)) + followButton.constrain([ + followButton.constraint(.top, toView: contentView,constant: 159), + followButton.constraint(.centerX, toView: contentView) + ]) + } + + func config(with account: Mastodon.Entity.Account) { + displayNameLabel.text = account.displayName.isEmpty ? account.username : account.displayName + acctLabel.text = account.acct + avatarImageView.af.setImage( + withURL: URL(string: account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + headerImageView.af.setImage( + withURL: URL(string: account.header)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct SearchRecommendAccountsCollectionViewCell_Previews: PreviewProvider { + + static var controls: some View { + Group { + UIViewPreview() { + let cell = SearchRecommendAccountsCollectionViewCell() + cell.avatarImageView.backgroundColor = .white + cell.headerImageView.backgroundColor = .red + cell.displayNameLabel.text = "sunxiaojian" + cell.acctLabel.text = "sunxiaojian@mastodon.online" + return cell + } + .previewLayout(.fixed(width: 257, height: 202)) + } + } + + static var previews: some View { + Group { + controls.colorScheme(.light) + controls.colorScheme(.dark) + } + .background(Color.gray) + } + +} + +#endif diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 108f2b6a2..adc9bd098 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -7,6 +7,7 @@ import Foundation import UIKit +import MastodonSDK class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let backgroundImageView: UIImageView = { @@ -18,8 +19,9 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let hashTagTitleLabel: UILabel = { let label = UILabel() label.textColor = .white - label.font = .preferredFont(forTextStyle: .caption1) + label.font = .systemFont(ofSize: 20, weight: .semibold) label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byTruncatingTail return label }() @@ -57,20 +59,60 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { extension SearchRecommendTagsCollectionViewCell { private func configure() { + backgroundColor = Asset.Colors.buttonDefault.color + layer.cornerRadius = 8 + clipsToBounds = true + contentView.addSubview(backgroundImageView) backgroundImageView.constrain(toSuperviewEdges: nil) contentView.addSubview(hashTagTitleLabel) - hashTagTitleLabel.pinTopLeft(padding: 16) + hashTagTitleLabel.pin(top: 16, left: 16, bottom: nil, right: 42) contentView.addSubview(peopleLabel) - peopleLabel.constrain([ - peopleLabel.constraint(toTop: contentView, constant: 46), - peopleLabel.constraint(toLeading: contentView, constant: 16) - ]) + peopleLabel.pinTopLeft(top: 46, left: 16) contentView.addSubview(flameIconView) flameIconView.pinTopRight(padding: 16) } + + func config(with tag: Mastodon.Entity.Tag) { + hashTagTitleLabel.text = "# " + tag.name + if let peopleAreTalking = tag.history?.compactMap({ Int($0.uses)}).reduce(0, +) { + let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) + peopleLabel.text = string + } else { + peopleLabel.text = "" + } + } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct SearchRecommendTagsCollectionViewCell_Previews: PreviewProvider { + + static var controls: some View { + Group { + UIViewPreview() { + let cell = SearchRecommendTagsCollectionViewCell() + cell.hashTagTitleLabel.text = "# test" + cell.peopleLabel.text = "128 people are talking" + 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/SearchViewController+recomendView.swift b/Mastodon/Scene/Search/SearchViewController+recomendView.swift index b498aa608..b62ebed7e 100644 --- a/Mastodon/Scene/Search/SearchViewController+recomendView.swift +++ b/Mastodon/Scene/Search/SearchViewController+recomendView.swift @@ -1,5 +1,5 @@ // -// SearchViewController+recommendView.swift +// SearchViewController+hashTagCollectionView.swift // Mastodon // // Created by sxiaojian on 2021/3/31. @@ -7,43 +7,121 @@ import Foundation import UIKit - +import OSLog +import MastodonSDK extension SearchViewController { - func setuprecommendView() { - recommendView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self)) - recommendView.dataSource = self - recommendView.delegate = self + func setupHashTagCollectionView() { + 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) + stackView.addArrangedSubview(header) + + hashTagCollectionView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self)) + hashTagCollectionView.delegate = self + + stackView.addArrangedSubview(hashTagCollectionView) + hashTagCollectionView.constrain([ + hashTagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130) + ]) + + viewModel.requestRecommendHashTags() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + if !self.viewModel.recommendHashTags.isEmpty { + let dataSource = RecomendHashTagSection.collectionViewDiffableDataSource(for: self.hashTagCollectionView) + var snapshot = NSDiffableDataSourceSnapshot() + 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() { + let header = SearchRecommendCollectionHeader() + header.titleLabel.text = L10n.Scene.Search.Recommend.Accounts.title + header.descriptionLabel.text = L10n.Scene.Search.Recommend.Accounts.description + header.seeAllButton.addTarget(self, action: #selector(SearchViewController.accountSeeAllButtonPressed(_:)), for: .touchUpInside) + stackView.addArrangedSubview(header) + + accountsCollectionView.register(SearchRecommendAccountsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self)) + accountsCollectionView.delegate = self + + stackView.addArrangedSubview(accountsCollectionView) + 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() { super.viewDidLayoutSubviews() - recommendView.collectionViewLayout.invalidateLayout() + hashTagCollectionView.collectionViewLayout.invalidateLayout() + accountsCollectionView.collectionViewLayout.invalidateLayout() } } 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) + } } -extension SearchViewController: UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { - return (self.viewModel.recommendAccounts.isEmpty ? 0 : 1) + (self.viewModel.recommendHashTags.isEmpty ? 0 : 1) +// MARK: - UICollectionViewDelegateFlowLayout +extension SearchViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - switch section { - case 0: - return viewModel.recommendHashTags.count - case 1: - return viewModel.recommendAccounts.count - default: - return 0 + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + if collectionView == hashTagCollectionView { + return 6 + } else { + return 12 } } - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - return UICollectionViewCell() + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + if collectionView == hashTagCollectionView { + return CGSize(width: 228, height: 130) + } else { + return CGSize(width: 257, height: 202) + } + } - +} + +extension SearchViewController { + @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 f76f596c0..c51350665 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -7,6 +7,7 @@ import UIKit import Combine +import MastodonSDK final class SearchViewController: UIViewController, NeedsDependency { @@ -27,7 +28,40 @@ final class SearchViewController: UIViewController, NeedsDependency { return searchBar }() - let recommendView: UICollectionView = { + let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.showsVerticalScrollIndicator = false + scrollView.alwaysBounceVertical = true + scrollView.clipsToBounds = false + return scrollView + }() + + let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 0 + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 68, right: 0) + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + let hashTagCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout) + view.backgroundColor = .clear + view.showsHorizontalScrollIndicator = false + view.showsVerticalScrollIndicator = false + view.layer.masksToBounds = false + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + var hashTagDiffableDataSource: UICollectionViewDiffableDataSource? + var accountDiffableDataSource: UICollectionViewDiffableDataSource? + + + let accountsCollectionView: UICollectionView = { let flowLayout = UICollectionViewFlowLayout() flowLayout.scrollDirection = .horizontal let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout) @@ -44,12 +78,35 @@ extension SearchViewController { override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = Asset.Colors.Background.search.color searchBar.delegate = self navigationItem.titleView = searchBar navigationItem.hidesBackButton = true - viewModel.requestRecommendData() + setupScrollView() + setupHashTagCollectionView() + setupAccountsCollectionView() + + } + func setupScrollView() { + view.addSubview(scrollView) + scrollView.constrain([ + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor) + ]) + + scrollView.addSubview(stackView) + stackView.constrain([ + stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + stackView.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), + scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + ]) + } - } extension SearchViewController: UISearchBarDelegate { @@ -79,3 +136,20 @@ extension SearchViewController: UISearchBarDelegate { extension SearchViewController { } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct SearchViewController_Previews: PreviewProvider { + + static var previews: some View { + UIViewControllerPreview { + let viewController = SearchViewController() + return viewController + } + .previewLayout(.fixed(width: 375, height: 800)) + } + +} + +#endif diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 679a2cfaf..40b22c880 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -5,14 +5,13 @@ // Created by sxiaojian on 2021/3/31. // -import Foundation import Combine +import Foundation import MastodonSDK -import UIKit import OSLog +import UIKit final class SearchViewModel { - var disposeBag = Set() // input @@ -25,30 +24,54 @@ final class SearchViewModel { var recommendAccounts = [Mastodon.Entity.Account]() init(context: AppContext) { - self.context = context + self.context = context } - func requestRecommendData() { - guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - let trendsAPI = context.apiService.recommendTrends(domain: activeMastodonAuthenticationBox.domain, query: Mastodon.API.Trends.Query(limit: 5)) - - let accountsAPI = context.apiService.recommendAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) - Publishers.Zip(trendsAPI,accountsAPI) - .sink { completion in - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: zip request fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: zip request success", ((#file as NSString).lastPathComponent), #line, #function) - break - } - } receiveValue: { [weak self] (tags, accounts) in - guard let self = self else { return } - self.recommendAccounts = accounts.value - self.recommendHashTags = tags.value + func requestRecommendHashTags() -> Future { + Future { promise in + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { + promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing))) + return } - .store(in: &disposeBag) + self.context.apiService.recommendTrends(domain: activeMastodonAuthenticationBox.domain, query: nil) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) + promise(.failure(error)) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request success", (#file as NSString).lastPathComponent, #line, #function) + promise(.success(())) + } + } receiveValue: { [weak self] tags in + guard let self = self else { return } + self.recommendHashTags = tags.value + } + .store(in: &self.disposeBag) + } + } + + func requestRecommendAccounts() -> Future { + Future { promise in + guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { + promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing))) + return + } + self.context.apiService.recommendAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) + promise(.failure(error)) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request success", (#file as NSString).lastPathComponent, #line, #function) + promise(.success(())) + } + } receiveValue: { [weak self] accounts in + guard let self = self else { return } + self.recommendAccounts = accounts.value + } + .store(in: &self.disposeBag) + } } } diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift new file mode 100644 index 000000000..5901b324a --- /dev/null +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -0,0 +1,89 @@ +// +// SearchRecommendCollectionHeader.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/1. +// + +import Foundation +import UIKit + +class SearchRecommendCollectionHeader: UIView { + let titleLabel: UILabel = { + let label = UILabel() + label.textColor = .black + label.font = .systemFont(ofSize: 20, weight: .semibold) + return label + }() + + let descriptionLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.lightSecondaryText.color + label.font = .preferredFont(forTextStyle: .body) + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + return label + }() + + 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) + return button + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +extension SearchRecommendCollectionHeader { + private func configure() { + backgroundColor = .clear + translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + titleLabel.pinTopLeft(top: 31, left: 16) + + addSubview(descriptionLabel) + descriptionLabel.constrain(toSuperviewEdges: UIEdgeInsets(top: 60, left: 16, bottom: 16, right: 16)) + + addSubview(seeAllButton) + seeAllButton.pinTopRight(top: 26, right: 16) + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct SearchRecommendCollectionHeader_Previews: PreviewProvider { + + static var controls: some View { + Group { + UIViewPreview() { + let cell = SearchRecommendCollectionHeader() + cell.titleLabel.text = "Trending in your timeline" + cell.descriptionLabel.text = "Hashtags that are getting quite a bit of attention among people you follow" + cell.seeAllButton.setTitle("See All", for: .normal) + return cell + } + .previewLayout(.fixed(width: 320, height: 116)) + } + } + + static var previews: some View { + Group { + controls.colorScheme(.light) + controls.colorScheme(.dark) + } + .background(Color.gray) + } + +} + +#endif diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 82fc9502b..db1913dc8 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -17,7 +17,16 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/account/) - public class Account: Codable { + public class Account: Codable, Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { + return lhs.id == rhs.id + } + public typealias ID = String @@ -82,5 +91,6 @@ extension Mastodon.Entity { case muteExpiresAt = "mute_expires_at" } } + } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift index 9bf1a3a28..4e3a66400 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift @@ -16,7 +16,7 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/history/) - public struct History: Codable { + public struct History: Codable, Hashable { /// UNIX timestamp on midnight of the given day public let day: Date public let uses: String diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift index 61c47ed68..867ff71a9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift @@ -16,7 +16,11 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/tag/) - public struct Tag: Codable { + public struct Tag: Codable, Hashable { + public static func == (lhs: Mastodon.Entity.Tag, rhs: Mastodon.Entity.Tag) -> Bool { + return lhs.name == rhs.name + } + // Base public let name: String public let url: String From f24aee739e4ef04bbaa46cfaff939b781954a02a Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 2 Apr 2021 12:10:12 +0800 Subject: [PATCH 2/4] chore: rename file name and code format --- Mastodon.xcodeproj/project.pbxproj | 8 ++-- ...hRecommendAccountsCollectionViewCell.swift | 14 +++---- ...earchRecommendTagsCollectionViewCell.swift | 9 ++--- ...> SearchViewController+RecomendView.swift} | 37 ++++++++----------- .../Scene/Search/SearchViewController.swift | 21 +++-------- .../SearchRecommendCollectionHeader.swift | 4 +- 6 files changed, 34 insertions(+), 59 deletions(-) rename Mastodon/Scene/Search/{SearchViewController+recomendView.swift => SearchViewController+RecomendView.swift} (91%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 19228f2e7..6a7995327 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -30,7 +30,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+RecomendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.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 */; }; @@ -344,7 +344,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+RecomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+RecomendView.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 = ""; }; @@ -1439,7 +1439,7 @@ children = ( 2DE0FAC62615F5D200CDF649 /* View */, DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, - 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */, + 2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, 2D34D9E026149C550081BFC0 /* CollectionViewCell */, ); @@ -2021,7 +2021,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+RecomendView.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index bed61f228..ba4babc52 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -6,8 +6,8 @@ // import Foundation -import UIKit import MastodonSDK +import UIKit class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let avatarImageView: UIImageView = { @@ -75,7 +75,7 @@ extension SearchRecommendAccountsCollectionViewCell { clipsToBounds = true contentView.addSubview(headerImageView) - headerImageView.pin(top: 16, left: 0, bottom: 0, right: 0 ) + headerImageView.pin(top: 16, left: 0, bottom: 0, right: 0) contentView.addSubview(avatarImageView) avatarImageView.pin(toSize: CGSize(width: 88, height: 88)) @@ -86,20 +86,20 @@ extension SearchRecommendAccountsCollectionViewCell { contentView.addSubview(displayNameLabel) displayNameLabel.constrain([ - displayNameLabel.constraint(.top, toView: contentView,constant: 108), + displayNameLabel.constraint(.top, toView: contentView, constant: 108), displayNameLabel.constraint(.centerX, toView: contentView) ]) contentView.addSubview(acctLabel) acctLabel.constrain([ - acctLabel.constraint(.top, toView: contentView,constant: 132), + acctLabel.constraint(.top, toView: contentView, constant: 132), acctLabel.constraint(.centerX, toView: contentView) ]) contentView.addSubview(followButton) followButton.pin(toSize: CGSize(width: 76, height: 24)) followButton.constrain([ - followButton.constraint(.top, toView: contentView,constant: 159), + followButton.constraint(.top, toView: contentView, constant: 159), followButton.constraint(.centerX, toView: contentView) ]) } @@ -124,10 +124,9 @@ extension SearchRecommendAccountsCollectionViewCell { import SwiftUI struct SearchRecommendAccountsCollectionViewCell_Previews: PreviewProvider { - static var controls: some View { Group { - UIViewPreview() { + UIViewPreview { let cell = SearchRecommendAccountsCollectionViewCell() cell.avatarImageView.backgroundColor = .white cell.headerImageView.backgroundColor = .red @@ -146,7 +145,6 @@ struct SearchRecommendAccountsCollectionViewCell_Previews: PreviewProvider { } .background(Color.gray) } - } #endif diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index adc9bd098..350720403 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -6,8 +6,8 @@ // import Foundation -import UIKit import MastodonSDK +import UIKit class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let backgroundImageView: UIImageView = { @@ -74,12 +74,11 @@ extension SearchRecommendTagsCollectionViewCell { contentView.addSubview(flameIconView) flameIconView.pinTopRight(padding: 16) - } func config(with tag: Mastodon.Entity.Tag) { hashTagTitleLabel.text = "# " + tag.name - if let peopleAreTalking = tag.history?.compactMap({ Int($0.uses)}).reduce(0, +) { + if let peopleAreTalking = tag.history?.compactMap({ Int($0.uses) }).reduce(0, +) { let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) peopleLabel.text = string } else { @@ -92,10 +91,9 @@ extension SearchRecommendTagsCollectionViewCell { import SwiftUI struct SearchRecommendTagsCollectionViewCell_Previews: PreviewProvider { - static var controls: some View { Group { - UIViewPreview() { + UIViewPreview { let cell = SearchRecommendTagsCollectionViewCell() cell.hashTagTitleLabel.text = "# test" cell.peopleLabel.text = "128 people are talking" @@ -112,7 +110,6 @@ struct SearchRecommendTagsCollectionViewCell_Previews: PreviewProvider { } .background(Color.gray) } - } #endif diff --git a/Mastodon/Scene/Search/SearchViewController+recomendView.swift b/Mastodon/Scene/Search/SearchViewController+RecomendView.swift similarity index 91% rename from Mastodon/Scene/Search/SearchViewController+recomendView.swift rename to Mastodon/Scene/Search/SearchViewController+RecomendView.swift index b62ebed7e..ca373b6b5 100644 --- a/Mastodon/Scene/Search/SearchViewController+recomendView.swift +++ b/Mastodon/Scene/Search/SearchViewController+RecomendView.swift @@ -1,14 +1,14 @@ // -// SearchViewController+hashTagCollectionView.swift +// SearchViewController+RecomendView.swift // Mastodon // // Created by sxiaojian on 2021/3/31. // import Foundation -import UIKit -import OSLog import MastodonSDK +import OSLog +import UIKit extension SearchViewController { func setupHashTagCollectionView() { @@ -17,15 +17,15 @@ extension SearchViewController { header.descriptionLabel.text = L10n.Scene.Search.Recommend.HashTag.description 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 - + stackView.addArrangedSubview(hashTagCollectionView) hashTagCollectionView.constrain([ hashTagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130) ]) - + viewModel.requestRecommendHashTags() .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -39,10 +39,10 @@ extension SearchViewController { self.hashTagDiffableDataSource = dataSource } } receiveValue: { _ in - } .store(in: &disposeBag) } + func setupAccountsCollectionView() { let header = SearchRecommendCollectionHeader() header.titleLabel.text = L10n.Scene.Search.Recommend.Accounts.title @@ -71,11 +71,10 @@ extension SearchViewController { self.accountDiffableDataSource = dataSource } } receiveValue: { _ in - } .store(in: &disposeBag) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() hashTagCollectionView.collectionViewLayout.invalidateLayout() @@ -85,16 +84,16 @@ extension SearchViewController { 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) + 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) } } // MARK: - UICollectionViewDelegateFlowLayout -extension SearchViewController: UICollectionViewDelegateFlowLayout { +extension SearchViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { @@ -104,24 +103,18 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout { return 12 } } - + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { if collectionView == hashTagCollectionView { return CGSize(width: 228, height: 130) } else { return CGSize(width: 257, height: 202) } - } - } extension SearchViewController { - @objc func hashTagSeeAllButtonPressed(_ sender: UIButton) { - - } - - @objc func accountSeeAllButtonPressed(_ 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 c51350665..856407f33 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -5,12 +5,11 @@ // Created by sxiaojian on 2021/3/31. // -import UIKit import Combine import MastodonSDK +import UIKit final class SearchViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -57,10 +56,10 @@ final class SearchViewController: UIViewController, NeedsDependency { view.translatesAutoresizingMaskIntoConstraints = false return view }() + var hashTagDiffableDataSource: UICollectionViewDiffableDataSource? var accountDiffableDataSource: UICollectionViewDiffableDataSource? - let accountsCollectionView: UICollectionView = { let flowLayout = UICollectionViewFlowLayout() flowLayout.scrollDirection = .horizontal @@ -75,7 +74,6 @@ final class SearchViewController: UIViewController, NeedsDependency { } extension SearchViewController { - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Asset.Colors.Background.search.color @@ -85,8 +83,8 @@ extension SearchViewController { setupScrollView() setupHashTagCollectionView() setupAccountsCollectionView() - } + func setupScrollView() { view.addSubview(scrollView) scrollView.constrain([ @@ -94,7 +92,7 @@ extension SearchViewController { scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor) + scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor), ]) scrollView.addSubview(stackView) @@ -105,7 +103,6 @@ extension SearchViewController { stackView.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), ]) - } } @@ -128,20 +125,13 @@ extension SearchViewController: UISearchBarDelegate { viewModel.searchText.send(searchText) } - func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { - - } -} - -extension SearchViewController { - + func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {} } #if canImport(SwiftUI) && DEBUG import SwiftUI struct SearchViewController_Previews: PreviewProvider { - static var previews: some View { UIViewControllerPreview { let viewController = SearchViewController() @@ -149,7 +139,6 @@ struct SearchViewController_Previews: PreviewProvider { } .previewLayout(.fixed(width: 375, height: 800)) } - } #endif diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index 5901b324a..02915ed25 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -62,10 +62,9 @@ extension SearchRecommendCollectionHeader { import SwiftUI struct SearchRecommendCollectionHeader_Previews: PreviewProvider { - static var controls: some View { Group { - UIViewPreview() { + UIViewPreview { let cell = SearchRecommendCollectionHeader() cell.titleLabel.text = "Trending in your timeline" cell.descriptionLabel.text = "Hashtags that are getting quite a bit of attention among people you follow" @@ -83,7 +82,6 @@ struct SearchRecommendCollectionHeader_Previews: PreviewProvider { } .background(Color.gray) } - } #endif From c0bd7c2497d9010205f6d06a30ba5db5a168e3f8 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 2 Apr 2021 16:50:07 +0800 Subject: [PATCH 3/4] chore: change .black to asset color. --- .../MastodonRegisterViewController.swift | 4 ++-- .../Register/MastodonRegisterViewModel.swift | 4 ++-- .../MastodonServerRulesViewController.swift | 2 +- .../MastodonServerRulesViewModel.swift | 2 +- ...SearchRecommendTagsCollectionViewCell.swift | 18 ++++++++++++++---- .../View/SearchRecommendCollectionHeader.swift | 2 +- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 2bfa138b4..6079f4d0c 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -58,7 +58,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let largeTitleLabel: UILabel = { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) - label.textColor = .black + label.textColor = Asset.Colors.Label.primary.color label.text = L10n.Scene.Register.title return label }() @@ -97,7 +97,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let domainLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .headline) - label.textColor = .black + label.textColor = Asset.Colors.Label.primary.color return label }() diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 45b4599a9..cd6106c23 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -185,9 +185,9 @@ extension MastodonRegisterViewModel { let attributeString = NSMutableAttributedString() let image = MastodonRegisterViewModel.checkmarkImage(font: font) - attributeString.append(attributedStringImage(with: image, tintColor: validateState == .valid ? .black : .clear)) + attributeString.append(attributedStringImage(with: image, tintColor: validateState == .valid ? Asset.Colors.Label.primary.color : .clear)) attributeString.append(NSAttributedString(string: " ")) - let eightCharactersDescription = NSAttributedString(string: L10n.Scene.Register.Input.Password.hint, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black]) + let eightCharactersDescription = NSAttributedString(string: L10n.Scene.Register.Input.Password.hint, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.Label.primary.color]) attributeString.append(eightCharactersDescription) return attributeString diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 86b9dc3eb..edc4c59e1 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -40,7 +40,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency let rulesLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .body) - label.textColor = .black + label.textColor = Asset.Colors.Label.primary.color label.text = "Rules" label.numberOfLines = 0 return label diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift index 14b4f0941..b1d000db5 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift @@ -40,7 +40,7 @@ final class MastodonServerRulesViewModel { let imageName = String(i + 1) + ".circle.fill" let image = UIImage(systemName: imageName, withConfiguration: configuration)! let attachment = NSTextAttachment() - attachment.image = image.withTintColor(.black) + attachment.image = image.withTintColor(Asset.Colors.Label.primary.color) let imageAttribute = NSAttributedString(attachment: attachment) let ruleString = NSAttributedString(string: " " + rule.text + "\n\n") diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 350720403..685d214e6 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -78,12 +78,22 @@ extension SearchRecommendTagsCollectionViewCell { func config(with tag: Mastodon.Entity.Tag) { hashTagTitleLabel.text = "# " + tag.name - if let peopleAreTalking = tag.history?.compactMap({ Int($0.uses) }).reduce(0, +) { - let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) - peopleLabel.text = string - } else { + guard let historys = tag.history else { peopleLabel.text = "" + return } + var recentHistory = [Mastodon.Entity.History]() + for history in historys { + if Int(history.uses) == 0 { + break + } else { + recentHistory.append(history) + } + } + let 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/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index 02915ed25..00efecd85 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -11,7 +11,7 @@ import UIKit class SearchRecommendCollectionHeader: UIView { let titleLabel: UILabel = { let label = UILabel() - label.textColor = .black + label.textColor = Asset.Colors.Label.primary.color label.font = .systemFont(ofSize: 20, weight: .semibold) return label }() From 608e916320b1a0797f8a62ad32451460cda346d1 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 2 Apr 2021 20:45:33 +0800 Subject: [PATCH 4/4] chore: remove extension from MastodonSDK --- Mastodon.xcodeproj/project.pbxproj | 12 +++++++++++ .../MastodonSDK/Mastodon+Entity+Account.swift | 18 +++++++++++++++++ .../MastodonSDK/Mastodon+Entity+History.swift | 20 +++++++++++++++++++ .../MastodonSDK/Mastodon+Entity+Tag.swift | 18 +++++++++++++++++ .../Entity/Mastodon+Entity+Account.swift | 11 +--------- .../Entity/Mastodon+Entity+History.swift | 2 +- .../Entity/Mastodon+Entity+Tag.swift | 11 +++++----- 7 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift create mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift create mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0bff57bed..a2ab8dd41 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -108,6 +108,9 @@ 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; + 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; + 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; + 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; }; 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; }; 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; }; 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; @@ -449,6 +452,9 @@ 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; 5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; + 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = ""; }; + 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = ""; }; + 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = ""; }; 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; @@ -1275,6 +1281,9 @@ isa = PBXGroup; children = ( DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */, + 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */, + 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */, + 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */, 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */, ); path = MastodonSDK; @@ -2048,6 +2057,7 @@ 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, + 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */, DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */, DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, @@ -2092,6 +2102,7 @@ DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, + 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */, DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */, DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */, DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */, @@ -2118,6 +2129,7 @@ 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, + 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */, 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, 5D0393902612D259007FE196 /* WebViewController.swift in Sources */, diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift new file mode 100644 index 000000000..8fd6bd67a --- /dev/null +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -0,0 +1,18 @@ +// +// Mastodon+Entity+Account.swift +// Mastodon +// +// Created by xiaojian sun on 2021/4/2. +// + +import MastodonSDK + +extension Mastodon.Entity.Account: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift new file mode 100644 index 000000000..b116889b8 --- /dev/null +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift @@ -0,0 +1,20 @@ +// +// Mastodon+Entity+History.swift +// Mastodon +// +// Created by xiaojian sun on 2021/4/2. +// + +import MastodonSDK + +extension Mastodon.Entity.History: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(uses) + hasher.combine(accounts) + hasher.combine(day) + } + + public static func == (lhs: Mastodon.Entity.History, rhs: Mastodon.Entity.History) -> Bool { + return lhs.uses == rhs.uses && lhs.uses == rhs.uses && lhs.day == rhs.day + } +} diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift new file mode 100644 index 000000000..caf819b38 --- /dev/null +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift @@ -0,0 +1,18 @@ +// +// Mastodon+Entity+Tag.swift +// Mastodon +// +// Created by xiaojian sun on 2021/4/2. +// + +import MastodonSDK + +extension Mastodon.Entity.Tag: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } + + public static func == (lhs: Mastodon.Entity.Tag, rhs: Mastodon.Entity.Tag) -> Bool { + return lhs.name == rhs.name + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index db1913dc8..13f3c0a71 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -17,16 +17,7 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/account/) - public class Account: Codable, Hashable { - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { - return lhs.id == rhs.id - } - + public class Account: Codable { public typealias ID = String diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift index 4e3a66400..9bf1a3a28 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift @@ -16,7 +16,7 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/history/) - public struct History: Codable, Hashable { + public struct History: Codable { /// UNIX timestamp on midnight of the given day public let day: Date public let uses: String diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift index 867ff71a9..e7f095eb3 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift @@ -16,15 +16,16 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/tag/) - public struct Tag: Codable, Hashable { - public static func == (lhs: Mastodon.Entity.Tag, rhs: Mastodon.Entity.Tag) -> Bool { - return lhs.name == rhs.name - } - + public struct Tag: Codable { // Base public let name: String public let url: String public let history: [History]? + enum CodingKeys: String, CodingKey { + case name + case url + case history + } } }