Merge branch 'develop' into feature/profile-coordinator
# Conflicts: # Mastodon.xcodeproj/project.pbxproj
This commit is contained in:
commit
af4fcf9dfd
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,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
|
|
@ -82,5 +82,6 @@ extension Mastodon.Entity {
|
|||
case muteExpiresAt = "mute_expires_at"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue