Merge branch 'develop' into feature/profile-coordinator

# Conflicts:
#	Mastodon.xcodeproj/project.pbxproj
This commit is contained in:
CMK 2021-04-07 14:44:58 +08:00
commit af4fcf9dfd
25 changed files with 797 additions and 151 deletions

View File

@ -286,6 +286,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"
}
}
}
}

View File

@ -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 */; };
@ -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 */; };
@ -104,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 */; };
@ -376,7 +383,7 @@
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+recomendView.swift"; sourceTree = "<group>"; };
2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+RecomendView.swift"; sourceTree = "<group>"; };
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = "<group>"; };
@ -437,6 +444,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>"; };
@ -450,6 +461,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 = "<group>"; };
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
@ -792,6 +806,7 @@
isa = PBXGroup;
children = (
2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */,
2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */,
);
path = CollectionViewCell;
sourceTree = "<group>";
@ -947,6 +962,8 @@
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
2DE0FAC02615F04D00CDF649 /* RecomendHashTagSection.swift */,
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
);
@ -1013,6 +1030,14 @@
path = Decoration;
sourceTree = "<group>";
};
2DE0FAC62615F5D200CDF649 /* View */ = {
isa = PBXGroup;
children = (
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */,
);
path = View;
sourceTree = "<group>";
};
2DF75BB725D1473400694EC8 /* Stack */ = {
isa = PBXGroup;
children = (
@ -1281,6 +1306,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;
@ -1505,8 +1533,9 @@
DB9D6BEE25E4F5370051B173 /* Search */ = {
isa = PBXGroup;
children = (
2DE0FAC62615F5D200CDF649 /* View */,
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */,
2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */,
2D34D9CA261489930081BFC0 /* SearchViewController+RecomendView.swift */,
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */,
2D34D9E026149C550081BFC0 /* CollectionViewCell */,
);
@ -2067,11 +2096,13 @@
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
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 */,
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
@ -2090,6 +2121,7 @@
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
@ -2112,6 +2144,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 */,
@ -2121,6 +2154,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 */,
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
@ -2137,6 +2171,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 */,
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
@ -2172,6 +2207,7 @@
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.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 */,
@ -2187,7 +2223,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 */,
DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */,
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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 }

View File

@ -43,6 +43,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 systemBackground = ColorAsset(name: "Colors/Background/system.background")

View File

@ -456,6 +456,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")

View File

@ -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
}
}

View File

@ -147,6 +147,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";

View File

@ -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
}()

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1,150 @@
//
// SearchRecommendAccountsCollectionViewCell.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/1.
//
import Foundation
import MastodonSDK
import UIKit
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.brandBlue.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

View File

@ -6,6 +6,7 @@
//
import Foundation
import MastodonSDK
import UIKit
class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
@ -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,67 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
extension SearchRecommendTagsCollectionViewCell {
private func configure() {
backgroundColor = Asset.Colors.brandBlue.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
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
}
}
#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

View File

@ -0,0 +1,120 @@
//
// SearchViewController+RecomendView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/31.
//
import Foundation
import MastodonSDK
import OSLog
import UIKit
extension SearchViewController {
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()
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)
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension SearchViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
if collectionView == hashTagCollectionView {
return 6
} else {
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) {}
}

View File

@ -1,49 +0,0 @@
//
// SearchViewController+recommendView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/31.
//
import Foundation
import UIKit
extension SearchViewController {
func setuprecommendView() {
recommendView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self))
recommendView.dataSource = self
recommendView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
recommendView.collectionViewLayout.invalidateLayout()
}
}
extension SearchViewController: UICollectionViewDelegate {
}
extension SearchViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return (self.viewModel.recommendAccounts.isEmpty ? 0 : 1) + (self.viewModel.recommendHashTags.isEmpty ? 0 : 1)
}
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, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return UICollectionViewCell()
}
}

View File

@ -5,11 +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) } }
@ -27,7 +27,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)
@ -41,15 +74,36 @@ final class SearchViewController: UIViewController, NeedsDependency {
}
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 {
@ -71,11 +125,20 @@ extension SearchViewController: UISearchBarDelegate {
viewModel.searchText.send(searchText)
}
func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {
func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {}
}
#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))
}
}
extension SearchViewController {
}
#endif

View File

@ -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)
}
}
}

View File

@ -0,0 +1,87 @@
//
// 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 = Asset.Colors.Label.primary.color
label.font = .systemFont(ofSize: 20, weight: .semibold)
return label
}()
let descriptionLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.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.brandBlue.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

View File

@ -82,5 +82,6 @@ extension Mastodon.Entity {
case muteExpiresAt = "mute_expires_at"
}
}
}

View File

@ -22,5 +22,10 @@ extension Mastodon.Entity {
public let url: String
public let history: [History]?
enum CodingKeys: String, CodingKey {
case name
case url
case history
}
}
}