feature: search recommend page
This commit is contained in:
parent
fa14477f7d
commit
458ab6bcda
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
|
||||
2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecomendHashTagSection.swift; sourceTree = "<group>"; };
|
||||
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = "<group>"; };
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||
2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProviderFacade.swift; sourceTree = "<group>"; };
|
||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewCellDelegate.swift"; sourceTree = "<group>"; };
|
||||
|
@ -718,6 +726,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */,
|
||||
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */,
|
||||
);
|
||||
path = CollectionViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
2DE0FAC62615F5D200CDF649 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
@ -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<RecomendHashTagSection, Mastodon.Entity.Tag> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, tag -> UICollectionViewCell? in
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self), for: indexPath) as! SearchRecommendTagsCollectionViewCell
|
||||
cell.config(with: tag)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<RecommendAccountSection, Mastodon.Entity.Account> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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<RecomendHashTagSection, Mastodon.Entity.Tag>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.viewModel.recommendHashTags, toSection: .main)
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
self.hashTagDiffableDataSource = dataSource
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
func setupAccountsCollectionView() {
|
||||
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<RecommendAccountSection, Mastodon.Entity.Account>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.viewModel.recommendAccounts, toSection: .main)
|
||||
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
self.accountDiffableDataSource = dataSource
|
||||
}
|
||||
} receiveValue: { _ in
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RecomendHashTagSection, Mastodon.Entity.Tag>?
|
||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, Mastodon.Entity.Account>?
|
||||
|
||||
|
||||
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
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
// 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<Void, Error> {
|
||||
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<Void, Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue