diff --git a/Localization/app.json b/Localization/app.json index fdac63c88..52f0db07b 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -464,8 +464,8 @@ } }, "suggestion_account": { - "title": "Find People to Follow", - "follow_explain": "When you follow someone, you’ll see their posts in your home feed." + "title": "Popular on Mastodon", + "follow_all": "Follow all" }, "compose": { "title": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 46d7ea619..7179d5f87 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -75,9 +75,6 @@ 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */; }; - 2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */; }; - 2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */; }; 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; @@ -149,6 +146,8 @@ D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; }; D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; }; D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; }; + D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; }; + D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; }; D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; }; D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; }; D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; }; @@ -416,7 +415,6 @@ DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */; }; DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; }; DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; }; - DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; }; @@ -441,7 +439,6 @@ DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; }; DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; }; DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.swift */; }; - DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */; }; DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */; }; DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */; }; DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.swift */; }; @@ -694,9 +691,6 @@ 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; }; 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = ""; }; - 2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountCollectionViewCell.swift; sourceTree = ""; }; - 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedAccountSection.swift; sourceTree = ""; }; - 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedAccountItem.swift; sourceTree = ""; }; 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = ""; }; 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = ""; }; 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = ""; }; @@ -801,6 +795,8 @@ D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = ""; }; D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = ""; }; D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; + D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = ""; }; + D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = ""; }; D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = ""; }; D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = ""; }; D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = ""; }; @@ -1135,7 +1131,6 @@ DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = ""; }; DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = ""; }; DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountItem.swift; sourceTree = ""; }; - DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountViewModel+Diffable.swift"; sourceTree = ""; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; }; DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; }; DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = ""; }; @@ -1168,7 +1163,6 @@ DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = ""; }; DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = ""; }; DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = ""; }; - DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = ""; }; DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+TableViewControllerNavigateable.swift"; sourceTree = ""; }; DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+StatusTableViewControllerNavigateable.swift"; sourceTree = ""; }; DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPostsViewController.swift; sourceTree = ""; }; @@ -1576,14 +1570,6 @@ path = Button; sourceTree = ""; }; - 2D4AD89A2631659400613EFC /* CollectionViewCell */ = { - isa = PBXGroup; - children = ( - 2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */, - ); - path = CollectionViewCell; - sourceTree = ""; - }; 2D59819925E4A55C000FB903 /* ConfirmEmail */ = { isa = PBXGroup; children = ( @@ -1625,10 +1611,8 @@ isa = PBXGroup; children = ( DB4F097826A039B400D62E92 /* Onboarding */, - DB0617FB27855B740030EE79 /* Account */, DB0617F827855B170030EE79 /* User */, DB0617F927855B460030EE79 /* Profile */, - DB0FCB892796BE1E006C02E2 /* RecommandAccount */, DB4F097926A039C400D62E92 /* Status */, DB65C63527A2AF52008BAC2E /* Report */, DB0617F727855B010030EE79 /* Notification */, @@ -1683,22 +1667,23 @@ 2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = { isa = PBXGroup; children = ( + 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, + DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */, 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */, 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */, - DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */, - 2D4AD89A2631659400613EFC /* CollectionViewCell */, - 2DAC9E43262FC9DE0062E1A6 /* TableViewCell */, + 2DAC9E43262FC9DE0062E1A6 /* TableView-Components */, ); path = SuggestionAccount; sourceTree = ""; }; - 2DAC9E43262FC9DE0062E1A6 /* TableViewCell */ = { + 2DAC9E43262FC9DE0062E1A6 /* TableView-Components */ = { isa = PBXGroup; children = ( 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */, - DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */, + D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */, + D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */, ); - path = TableViewCell; + path = "TableView-Components"; sourceTree = ""; }; 2DE0FAC62615F5D200CDF649 /* View */ = { @@ -1907,15 +1892,6 @@ path = Settings; sourceTree = ""; }; - DB0617FB27855B740030EE79 /* Account */ = { - isa = PBXGroup; - children = ( - 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */, - 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */, - ); - path = Account; - sourceTree = ""; - }; DB0618082785B2790030EE79 /* Cell */ = { isa = PBXGroup; children = ( @@ -1943,15 +1919,6 @@ path = View; sourceTree = ""; }; - DB0FCB892796BE1E006C02E2 /* RecommandAccount */ = { - isa = PBXGroup; - children = ( - 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, - DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */, - ); - path = RecommandAccount; - sourceTree = ""; - }; DB1D187125EF5BBD003F1F23 /* TableView */ = { isa = PBXGroup; children = ( @@ -3563,6 +3530,7 @@ DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */, 62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */, DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */, + D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */, 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */, DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */, DB5B54B22833C24B00DEF8B2 /* RebloggedByViewController+DataSourceProvider.swift in Sources */, @@ -3618,7 +3586,6 @@ DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */, - DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */, 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */, @@ -3632,7 +3599,6 @@ DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */, DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */, DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */, - DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */, DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */, DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */, DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */, @@ -3689,7 +3655,6 @@ DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */, DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */, DBF1572F27046F1A00EC00B7 /* SecondaryPlaceholderViewController.swift in Sources */, - 2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */, DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */, 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */, DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */, @@ -3856,7 +3821,6 @@ DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, - 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */, @@ -3888,6 +3852,7 @@ DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, + D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */, @@ -3927,7 +3892,6 @@ DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */, DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, - 2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */, DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */, 85BC11B32932414900E191CD /* AltTextViewController.swift in Sources */, DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */, diff --git a/Mastodon/Diffable/Account/SelectedAccountItem.swift b/Mastodon/Diffable/Account/SelectedAccountItem.swift deleted file mode 100644 index 05ecdae8d..000000000 --- a/Mastodon/Diffable/Account/SelectedAccountItem.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// SelectedAccountItem.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/22. -// - -import CoreData -import Foundation -import CoreDataStack - -enum SelectedAccountItem: Hashable { - case account(ManagedObjectRecord) - case placeHolder(uuid: UUID) -} diff --git a/Mastodon/Diffable/Account/SelectedAccountSection.swift b/Mastodon/Diffable/Account/SelectedAccountSection.swift deleted file mode 100644 index d71cbf326..000000000 --- a/Mastodon/Diffable/Account/SelectedAccountSection.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// SelectedAccountSection.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/22. -// - -import UIKit -import CoreData -import CoreDataStack -import MastodonCore -import MastodonSDK - -enum SelectedAccountSection: Equatable, Hashable { - case main -} - -extension SelectedAccountSection { - static func collectionViewDiffableDataSource( - collectionView: UICollectionView, - context: AppContext - ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self), for: indexPath) as! SuggestionAccountCollectionViewCell - switch item { - case .account(let record): - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - cell.config(with: user) - } - case .placeHolder: - cell.configAsPlaceHolder() - } - return cell - } - } -} diff --git a/Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift b/Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift deleted file mode 100644 index e5aa0a605..000000000 --- a/Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// RecommendAccountSection.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/1. -// - -import CoreData -import CoreDataStack -import Foundation -import MastodonSDK -import UIKit -import MetaTextKit -import MastodonMeta -import Combine -import MastodonCore - -enum RecommendAccountSection: Equatable, Hashable { - case main -} - -//extension RecommendAccountSection { -// static func collectionViewDiffableDataSource( -// for collectionView: UICollectionView, -// dependency: NeedsDependency, -// delegate: SearchRecommendAccountsCollectionViewCellDelegate, -// managedObjectContext: NSManagedObjectContext -// ) -> UICollectionViewDiffableDataSource { -// UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in -// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell -// managedObjectContext.performAndWait { -// let user = managedObjectContext.object(with: objectID) as! MastodonUser -// configure(cell: cell, user: user, dependency: dependency) -// } -// cell.delegate = delegate -// return cell -// } -// } -// -// static func configure( -// cell: SearchRecommendAccountsCollectionViewCell, -// user: MastodonUser, -// dependency: NeedsDependency -// ) { -// configureContent(cell: cell, user: user) -// -// if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user { -// configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton) -// } -// -// Publishers.CombineLatest( -// ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error }, -// dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self) -// ) -// .receive(on: DispatchQueue.main) -// .sink { _ in -// // do nothing -// } receiveValue: { [weak cell] change, authentication in -// guard let cell = cell else { return } -// guard case .update(let object) = change.changeType, -// let user = object as? MastodonUser else { return } -// guard let currentMastodonUser = authentication?.user else { return } -// -// configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton) -// } -// .store(in: &cell.disposeBag) -// -// } -// -// static func configureContent( -// cell: SearchRecommendAccountsCollectionViewCell, -// user: MastodonUser -// ) { -// do { -// let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) -// let metaContent = try MastodonMetaContent.convert(document: mastodonContent) -// cell.displayNameLabel.configure(content: metaContent) -// } catch { -// let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback) -// cell.displayNameLabel.configure(content: metaContent) -// } -// cell.acctLabel.text = "@" + user.acct -// cell.avatarImageView.af.setImage( -// withURL: user.avatarImageURLWithFallback(domain: user.domain), -// placeholderImage: UIImage.placeholder(color: .systemFill), -// imageTransition: .crossDissolve(0.2) -// ) -// cell.headerImageView.af.setImage( -// withURL: URL(string: user.header)!, -// placeholderImage: UIImage.placeholder(color: .systemFill), -// imageTransition: .crossDissolve(0.2) -// ) -// } -// -// static func configureFollowButton( -// with mastodonUser: MastodonUser, -// currentMastodonUser: MastodonUser, -// followButton: HighlightDimmableButton -// ) { -// let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser) -// followButton.setTitle(relationshipActionSet.title, for: .normal) -// } -// -// static func relationShipActionSet( -// mastodonUser: MastodonUser, -// currentMastodonUser: MastodonUser -// ) -> ProfileViewModel.RelationshipActionOptionSet { -// var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow]) -// let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false -// if isFollowing { -// relationshipActionSet.insert(.following) -// } -// -// let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false -// if isPending { -// relationshipActionSet.insert(.pending) -// } -// -// let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false -// if isBlocking { -// relationshipActionSet.insert(.blocking) -// } -// -// let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false -// if isBlockedBy { -// relationshipActionSet.insert(.blocked) -// } -// return relationshipActionSet -// } -// -//} -// -extension RecommendAccountSection { - - struct Configuration { - let authContext: AuthContext - weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate? - } - - static func tableViewDiffableDataSource( - tableView: UITableView, - context: AppContext, - configuration: Configuration - ) -> UITableViewDiffableDataSource { - UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell - switch item { - case .account(let record): - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - cell.configure(user: user) - } - - cell.viewModel.userIdentifier = configuration.authContext.mastodonAuthenticationBox - cell.delegate = configuration.suggestionAccountTableViewCellDelegate - } - return cell - } - } - -} diff --git a/Mastodon/Diffable/Search/SearchHistorySection.swift b/Mastodon/Diffable/Search/SearchHistorySection.swift index 5d1db9c55..813c5b59a 100644 --- a/Mastodon/Diffable/Search/SearchHistorySection.swift +++ b/Mastodon/Diffable/Search/SearchHistorySection.swift @@ -32,7 +32,12 @@ extension SearchHistorySection { guard let user = item.object(in: context.managedObjectContext) else { return } cell.configure( me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, - viewModel: .init(value: user, followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher()), + viewModel: SearchHistoryUserCollectionViewCell.ViewModel( + value: user, + followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), + blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), + followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() + ), delegate: configuration.searchHistorySectionHeaderCollectionReusableViewDelegate ) } diff --git a/Mastodon/Diffable/Search/SearchResultSection.swift b/Mastodon/Diffable/Search/SearchResultSection.swift index 2a6e92097..90560150e 100644 --- a/Mastodon/Diffable/Search/SearchResultSection.swift +++ b/Mastodon/Diffable/Search/SearchResultSection.swift @@ -53,7 +53,10 @@ extension SearchResultSection { authContext: authContext, tableView: tableView, cell: cell, - viewModel: .init(value: .user(user), followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher()), + viewModel: UserTableViewCell.ViewModel(value: .user(user), + followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), + blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), + followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), configuration: configuration ) } diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index e45c1fa1f..cbad1ff72 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -48,7 +48,11 @@ extension UserSection { authContext: authContext, tableView: tableView, cell: cell, - viewModel: .init(value: .user(user), followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher()), + viewModel: UserTableViewCell.ViewModel(value: .user(user), + followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), + blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), + followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() + ), configuration: configuration ) } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 2c221bc9b..88503ae7b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -32,7 +32,7 @@ extension DataSourceFacade { static func responseToUserFollowRequestAction( dependency: NeedsDependency & AuthContextProvider, notification: ManagedObjectRecord, - query: Mastodon.API.Account.FollowReqeustQuery + query: Mastodon.API.Account.FollowRequestQuery ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index f0a1a51a4..8b1a5c84d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -22,6 +22,17 @@ extension DataSourceFacade { if let userObject = user.object(in: dependency.context.managedObjectContext) { dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(userObject.id) } + + case .request: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + if let userObject = user.object(in: dependency.context.managedObjectContext) { + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(userObject.id) + } + case .unfollow: try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, @@ -39,6 +50,16 @@ extension DataSourceFacade { if let userObject = user.object(in: dependency.context.managedObjectContext) { dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(userObject.id) } + + case .pending: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + if let userObject = user.object(in: dependency.context.managedObjectContext) { + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == userObject.id }) + } case .none, .loading: break //no-op } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 7e22c346f..77ff7408c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -240,6 +240,19 @@ extension HomeTimelineViewController { .sink { [weak self] isEmpty in if isEmpty { self?.showEmptyView() + + let userDoesntFollowPeople: Bool + if let managedObjectContext = self?.context.managedObjectContext, + let me = self?.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user { + userDoesntFollowPeople = me.followersCount == 0 + } else { + userDoesntFollowPeople = true + } + + if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople { + self?.findPeopleButtonPressed(self) + self?.viewModel.presentedSuggestions = true + } } else { self?.emptyView.removeFromSuperview() } @@ -285,8 +298,6 @@ extension HomeTimelineViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.send() - if let timestamp = viewModel.lastAutomaticFetchTimestamp { let now = Date() if now.timeIntervalSince(timestamp) > 60 { @@ -360,6 +371,7 @@ extension HomeTimelineViewController { bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8), + manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), ]) let buttonContainerStackView = UIStackView() @@ -374,9 +386,10 @@ extension HomeTimelineViewController { } } +//MARK: - Actions extension HomeTimelineViewController { - @objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) { + @objc private func findPeopleButtonPressed(_ sender: Any?) { let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext) suggestionAccountViewModel.delegate = viewModel _ = coordinator.present( @@ -387,13 +400,11 @@ extension HomeTimelineViewController { } @objc private func manuallySearchButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let searchDetailViewModel = SearchDetailViewModel(authContext: viewModel.authContext) _ = coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let setting = context.settingService.currentSetting.value else { return } let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting) _ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 7319409e5..663a38724 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -30,7 +30,8 @@ final class HomeTimelineViewModel: NSObject { let fetchedResultsController: FeedFetchedResultsController let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let listBatchFetchViewModel = ListBatchFetchViewModel() - let viewDidAppear = PassthroughSubject() + + var presentedSuggestions = false @Published var lastAutomaticFetchTimestamp: Date? = nil @Published var scrollPositionRecord: ScrollPositionRecord? = nil diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 01ee5acfd..8f1cbecf2 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -301,7 +301,7 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { dataSource?.apply(snapshot, animatingDifferences: false) - OperationQueue.main.addOperation { + DispatchQueue.main.async { let numberOfResults = viewModel.filteredServers.count self.contentView.updateCorners(numberOfResults: numberOfResults) } diff --git a/Mastodon/Scene/Onboarding/PickServer/PickServerSection.swift b/Mastodon/Scene/Onboarding/PickServer/PickServerSection.swift index 6dfd81c59..9f1781657 100644 --- a/Mastodon/Scene/Onboarding/PickServer/PickServerSection.swift +++ b/Mastodon/Scene/Onboarding/PickServer/PickServerSection.swift @@ -65,7 +65,7 @@ extension PickServerSection { }() if let proxiedThumbnail = server.proxiedThumbnail, let thumbnailUrl = URL(string: proxiedThumbnail) { cell.thumbnailImageView.af.setImage(withURL: thumbnailUrl, completion: { _ in - OperationQueue.main.addOperation { + DispatchQueue.main.async { cell.thumbnailImageView.isHidden = false } }) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 3bb6971bb..5f8e878a1 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -280,8 +280,6 @@ extension ProfileHeaderViewController { } func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) - // set title view offset let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 6ebeec898..033e54615 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -289,6 +289,12 @@ extension ProfileViewController { bindPager() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.navigationBar.prefersLargeTitles = false + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -374,7 +380,7 @@ extension ProfileViewController { profileHeaderViewController.profileHeaderView.viewModel.$name .receive(on: DispatchQueue.main) .sink { [weak self] name in - guard let self = self else { return } + guard let self = self, self.isModal == false else { return } self.navigationItem.title = name } .store(in: &disposeBag) @@ -422,7 +428,7 @@ extension ProfileViewController { } } receiveValue: { [weak self] menu in guard let self = self else { return } - OperationQueue.main.addOperation { + DispatchQueue.main.async { self.moreMenuBarButtonItem.menu = menu } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift index c9f66206c..e31f050bd 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift @@ -16,10 +16,12 @@ extension SearchHistoryUserCollectionViewCell { let followedUsers: AnyPublisher<[String], Never> let blockedUsers: AnyPublisher<[String], Never> - - init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>) { + let followRequestedUsers: AnyPublisher<[String], Never> + + init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { self.value = value self.followedUsers = followedUsers + self.followRequestedUsers = followRequestedUsers self.blockedUsers = blockedUsers } } @@ -45,25 +47,26 @@ extension SearchHistoryUserCollectionViewCell { userView.setButtonState(.loading) } - Publishers.CombineLatest( + Publishers.CombineLatest3( viewModel.followedUsers, + viewModel.followRequestedUsers, viewModel.blockedUsers ) .receive(on: DispatchQueue.main) - .sink { [weak self] followed, blocked in - if blocked.contains(where: { $0 == user.id }) { + .sink { [weak self] followed, requested, blocked in + if blocked.contains(user.id) { self?.userView.setButtonState(.blocked) - } else if followed.contains(where: { $0 == user.id }) { + } else if followed.contains(user.id) { self?.userView.setButtonState(.unfollow) - } else { + } else if requested.contains(user.id) { + self?.userView.setButtonState(.pending) + } else if user.locked { + self?.userView.setButtonState(.request) + } else if user != me { self?.userView.setButtonState(.follow) } - - self?.setNeedsLayout() - self?.setNeedsUpdateConstraints() - self?.layoutIfNeeded() } .store(in: &_disposeBag) - + } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift index 2702d726b..c3455fd0e 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift @@ -33,7 +33,6 @@ extension StatusTableViewCell { if statusView.frame == .zero { // set status view width statusView.frame.size.width = tableView.frame.width - containerViewHorizontalMargin - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") } switch viewModel.value { diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 5cf498fdb..16285ebeb 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -15,8 +15,6 @@ import MastodonUI final class StatusTableViewCell: UITableViewCell { static let marginForRegularHorizontalSizeClass: CGFloat = 64 - - let logger = Logger(subsystem: "StatusTableViewCell", category: "View") weak var delegate: StatusTableViewCellDelegate? var disposeBag = Set() @@ -44,11 +42,6 @@ final class StatusTableViewCell: UITableViewCell { super.init(coder: coder) _init() } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - } extension StatusTableViewCell { diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 2defc8296..b83b8b47d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -16,10 +16,12 @@ extension UserTableViewCell { let followedUsers: AnyPublisher<[String], Never> let blockedUsers: AnyPublisher<[String], Never> + let followRequestedUsers: AnyPublisher<[String], Never> - init(value: Value, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>) { + init(value: Value, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { self.value = value self.followedUsers = followedUsers + self.followRequestedUsers = followRequestedUsers self.blockedUsers = blockedUsers } @@ -52,20 +54,24 @@ extension UserTableViewCell { userView.setButtonState(.loading) } - Publishers.CombineLatest( + Publishers.CombineLatest3( viewModel.followedUsers, + viewModel.followRequestedUsers, viewModel.blockedUsers ) .receive(on: DispatchQueue.main) - .sink { [weak self] followed, blocked in + .sink { [weak self] followed, requested, blocked in if blocked.contains(user.id) { self?.userView.setButtonState(.blocked) } else if followed.contains(user.id) { self?.userView.setButtonState(.unfollow) + } else if requested.contains(user.id) { + self?.userView.setButtonState(.pending) + } else if user.locked { + self?.userView.setButtonState(.request) } else if user != me { self?.userView.setButtonState(.follow) } - } .store(in: &disposeBag) diff --git a/Mastodon/Scene/SuggestionAccount/CollectionViewCell/SuggestionAccountCollectionViewCell.swift b/Mastodon/Scene/SuggestionAccount/CollectionViewCell/SuggestionAccountCollectionViewCell.swift deleted file mode 100644 index 296935521..000000000 --- a/Mastodon/Scene/SuggestionAccount/CollectionViewCell/SuggestionAccountCollectionViewCell.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// SuggestionAccountCollectionViewCell.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/22. -// - -import CoreDataStack -import Foundation -import UIKit -import MastodonAsset -import MastodonLocalization - -class SuggestionAccountCollectionViewCell: UICollectionViewCell { - let imageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = Asset.Colors.Label.tertiary.color - imageView.layer.cornerRadius = 4 - imageView.clipsToBounds = true - imageView.image = UIImage.placeholder(color: .systemFill) - return imageView - }() - - func configAsPlaceHolder() { - imageView.tintColor = Asset.Colors.Label.tertiary.color - imageView.image = UIImage.placeholder(color: .systemFill) - } - - func config(with mastodonUser: MastodonUser) { - imageView.af.setImage( - withURL: URL(string: mastodonUser.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) - } - - override func prepareForReuse() { - super.prepareForReuse() - } - - override init(frame: CGRect) { - super.init(frame: .zero) - configure() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configure() - } -} - -extension SuggestionAccountCollectionViewCell { - private func configure() { - contentView.addSubview(imageView) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.pinToParent() - } -} diff --git a/Mastodon/Diffable/RecommandAccount/RecommendAccountItem.swift b/Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift similarity index 100% rename from Mastodon/Diffable/RecommandAccount/RecommendAccountItem.swift rename to Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift diff --git a/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift b/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift new file mode 100644 index 000000000..c3f477a96 --- /dev/null +++ b/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift @@ -0,0 +1,54 @@ +// +// RecommendAccountSection.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/1. +// + +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import UIKit +import MetaTextKit +import MastodonMeta +import Combine +import MastodonCore + +enum RecommendAccountSection: Equatable, Hashable { + case main +} + +extension RecommendAccountSection { + + struct Configuration { + let authContext: AuthContext + weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate? + } + + static func tableViewDiffableDataSource( + tableView: UITableView, + context: AppContext, + configuration: Configuration + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell + switch item { + case .account(let record): + cell.delegate = configuration.suggestionAccountTableViewCellDelegate + context.managedObjectContext.performAndWait { + guard let user = record.object(in: context.managedObjectContext) else { return } + cell.configure(viewModel: + SuggestionAccountTableViewCell.ViewModel( + user: user, + followedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds, + blockedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds, + followRequestedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs) + ) + } + } + return cell + } + } + +} diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 7eacdc205..4962d3e71 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -9,7 +9,6 @@ import Combine import CoreData import CoreDataStack import Foundation -import OSLog import UIKit import MastodonAsset import MastodonCore @@ -17,71 +16,28 @@ import MastodonUI import MastodonLocalization class SuggestionAccountViewController: UIViewController, NeedsDependency { - - static let collectionViewHeight: CGFloat = 24 + 64 + 24 - + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() var viewModel: SuggestionAccountViewModel! - - private static func createCollectionViewLayout() -> UICollectionViewLayout { - let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(64), heightDimension: .absolute(64)) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item]) - - let section = NSCollectionLayoutSection(group: group) - section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 0, bottom: 24, trailing: 0) - section.orthogonalScrollingBehavior = .continuous - section.contentInsetsReference = .readableContent - section.interGroupSpacing = 16 - - return UICollectionViewCompositionalLayout(section: section) - } - - let collectionView: UICollectionView = { - let collectionViewLayout = SuggestionAccountViewController.createCollectionViewLayout() - let view = ControlContainableCollectionView( - frame: .zero, - collectionViewLayout: collectionViewLayout - ) - view.register(SuggestionAccountCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self)) - view.backgroundColor = .clear - view.showsHorizontalScrollIndicator = false - view.showsVerticalScrollIndicator = false - view.layer.masksToBounds = false - return view - }() let tableView: UITableView = { - let tableView = ControlContainableTableView() - tableView.register(SuggestionAccountTableViewCell.self, forCellReuseIdentifier: String(describing: SuggestionAccountTableViewCell.self)) - tableView.rowHeight = UITableView.automaticDimension - tableView.tableFooterView = UIView() - tableView.separatorStyle = .singleLine - tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + let tableView = UITableView(frame: .zero, style: .insetGrouped) + tableView.register(SuggestionAccountTableViewCell.self, forCellReuseIdentifier: SuggestionAccountTableViewCell.reuseIdentifier) + // we're lazy, that's why we don't put the Footer in tableViewFooter + tableView.register(SuggestionAccountTableViewFooter.self, forHeaderFooterViewReuseIdentifier: SuggestionAccountTableViewFooter.reuseIdentifier) + tableView.contentInset.top = 16 return tableView }() - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", (#file as NSString).lastPathComponent, #line, #function) - } -} - -extension SuggestionAccountViewController { override func viewDidLoad() { super.viewDidLoad() - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) + setupNavigationBarAppearance() + defer { setupNavigationBarBackgroundView() } + title = L10n.Scene.SuggestionAccount.title navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -89,66 +45,40 @@ extension SuggestionAccountViewController { target: self, action: #selector(SuggestionAccountViewController.doneButtonDidClick(_:)) ) - - collectionView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(collectionView) - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - collectionView.heightAnchor.constraint(equalToConstant: SuggestionAccountViewController.collectionViewHeight), - ]) - defer { view.bringSubviewToFront(collectionView) } tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: collectionView.bottomAnchor), + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - collectionView.delegate = self - viewModel.setupDiffableDataSource( - collectionView: collectionView - ) - tableView.delegate = self viewModel.setupDiffableDataSource( tableView: tableView, suggestionAccountTableViewCellDelegate: self ) + + view.backgroundColor = .secondarySystemBackground + tableView.backgroundColor = .secondarySystemBackground } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + + navigationController?.navigationBar.prefersLargeTitles = true + navigationItem.largeTitleDisplayMode = .automatic + tableView.deselectRow(with: transitionCoordinator, animated: animated) } - private func setupBackgroundColor(theme: Theme) { - view.backgroundColor = theme.systemBackgroundColor - collectionView.backgroundColor = theme.systemGroupedBackgroundColor - } -} + //MARK: - Actions -// MARK: - UICollectionViewDelegateFlowLayout -extension SuggestionAccountViewController: UICollectionViewDelegate { - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { -// guard let diffableDataSource = viewModel.collectionDiffableDataSource else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// switch item { -// case .accountObjectID(let accountObjectID): -// let mastodonUser = context.managedObjectContext.object(with: accountObjectID) as! MastodonUser -// let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser) -// DispatchQueue.main.async { -// self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) -// } -// default: -// break -// } + @objc func doneButtonDidClick(_ sender: UIButton) { + viewModel.delegate?.homeTimelineNeedRefresh.send() + dismiss(animated: true, completion: nil) } } @@ -168,6 +98,15 @@ extension SuggestionAccountViewController: UITableViewDelegate { ) } } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + guard let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SuggestionAccountTableViewFooter.reuseIdentifier) as? SuggestionAccountTableViewFooter else { + return nil + } + + footerView.delegate = self + return footerView + } } // MARK: - AuthContextProvider @@ -175,39 +114,21 @@ extension SuggestionAccountViewController: AuthContextProvider { var authContext: AuthContext { viewModel.authContext } } +// MARK: - UserTableViewCellDelegate +extension SuggestionAccountViewController: UserTableViewCellDelegate {} + // MARK: - SuggestionAccountTableViewCellDelegate -extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate { - func suggestionAccountTableViewCell( - _ cell: SuggestionAccountTableViewCell, - friendshipDidPressed button: UIButton - ) { - guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return } - guard let indexPath = tableView.indexPath(for: cell) else { return } - guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return } - - switch item { - case .account(let user): - Task { @MainActor in - cell.startAnimating() - do { - try await DataSourceFacade.responseToUserFollowAction( - dependency: self, - user: user - ) - } catch { - // do noting - } - cell.stopAnimating() - } // end Task +extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate { } + + +extension SuggestionAccountViewController: SuggestionAccountTableViewFooterDelegate { + func followAll(_ footerView: SuggestionAccountTableViewFooter) { + viewModel.followAllSuggestedAccounts(self) { + DispatchQueue.main.async { + self.dismiss(animated: true) + } } } } -extension SuggestionAccountViewController { - @objc func doneButtonDidClick(_ sender: UIButton) { - dismiss(animated: true, completion: nil) -// if viewModel.selectedAccounts.value.count > 0 { -// viewModel.delegate?.homeTimelineNeedRefresh.send() -// } - } -} +extension SuggestionAccountViewController: OnboardingViewControllerAppearance { } diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift deleted file mode 100644 index afb3eb7ec..000000000 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// SuggestionAccountViewModel+Diffable.swift -// Mastodon -// -// Created by MainasuK on 2022-2-10. -// - -import UIKit - -extension SuggestionAccountViewModel { - - func setupDiffableDataSource( - tableView: UITableView, - suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate - ) { - tableViewDiffableDataSource = RecommendAccountSection.tableViewDiffableDataSource( - tableView: tableView, - context: context, - configuration: RecommendAccountSection.Configuration( - authContext: authContext, - suggestionAccountTableViewCellDelegate: suggestionAccountTableViewCellDelegate - ) - ) - - userFetchedResultsController.$records - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] records in - guard let self = self else { return } - guard let tableViewDiffableDataSource = self.tableViewDiffableDataSource else { return } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - let items: [RecommendAccountItem] = records.map { RecommendAccountItem.account($0) } - snapshot.appendItems(items, toSection: .main) - - tableViewDiffableDataSource.applySnapshotUsingReloadData(snapshot, completion: nil) - } - .store(in: &disposeBag) - } - - func setupDiffableDataSource( - collectionView: UICollectionView - ) { - collectionViewDiffableDataSource = SelectedAccountSection.collectionViewDiffableDataSource( - collectionView: collectionView, - context: context - ) - - selectedUserFetchedResultsController.$records - .receive(on: DispatchQueue.main) - .sink { [weak self] records in - guard let self = self else { return } - guard let collectionViewDiffableDataSource = self.collectionViewDiffableDataSource else { return } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - var items: [SelectedAccountItem] = records.map { SelectedAccountItem.account($0) } - - if items.count < 10 { - let count = 10 - items.count - let placeholderItems: [SelectedAccountItem] = (0..() // output - var collectionViewDiffableDataSource: UICollectionViewDiffableDataSource? var tableViewDiffableDataSource: UITableViewDiffableDataSource? init( @@ -46,26 +43,16 @@ final class SuggestionAccountViewModel: NSObject { domain: nil, additionalPredicate: nil ) - self.selectedUserFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: nil, - additionalPredicate: nil - ) super.init() userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain - selectedUserFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain - selectedUserFetchedResultsController.additionalPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [ - MastodonUser.predicate(followingBy: authContext.mastodonAuthenticationBox.userID), - MastodonUser.predicate(followRequestedBy: authContext.mastodonAuthenticationBox.userID) - ]) - - // fetch recomment users + + // fetch recommended users Task { var userIDs: [MastodonUser.ID] = [] do { let response = try await context.apiService.suggestionAccountV2( - query: nil, + query: .init(limit: 5), authenticationBox: authContext.mastodonAuthenticationBox ) userIDs = response.value.map { $0.account.id } @@ -76,12 +63,11 @@ final class SuggestionAccountViewModel: NSObject { ) userIDs = response.value.map { $0.id } } catch { - os_log("%{public}s[%{public}ld], %{public}s: fetch recommendAccountV2 failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) + } guard !userIDs.isEmpty else { return } userFetchedResultsController.userIDs = userIDs - selectedUserFetchedResultsController.userIDs = userIDs } // fetch relationship @@ -99,4 +85,56 @@ final class SuggestionAccountViewModel: NSObject { .store(in: &disposeBag) } + func setupDiffableDataSource( + tableView: UITableView, + suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate + ) { + tableViewDiffableDataSource = RecommendAccountSection.tableViewDiffableDataSource( + tableView: tableView, + context: context, + configuration: RecommendAccountSection.Configuration( + authContext: authContext, + suggestionAccountTableViewCellDelegate: suggestionAccountTableViewCellDelegate + ) + ) + + userFetchedResultsController.$records + .receive(on: DispatchQueue.main) + .sink { [weak self] records in + guard let self = self else { return } + guard let tableViewDiffableDataSource = self.tableViewDiffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let items: [RecommendAccountItem] = records.map { RecommendAccountItem.account($0) } + snapshot.appendItems(items, toSection: .main) + + tableViewDiffableDataSource.applySnapshotUsingReloadData(snapshot) + } + .store(in: &disposeBag) + } + + func followAllSuggestedAccounts(_ dependency: NeedsDependency & AuthContextProvider, completion: (() -> Void)? = nil) { + + let userRecords = userFetchedResultsController.records.compactMap { + $0.object(in: dependency.context.managedObjectContext)?.asRecord + } + + Task { + await withTaskGroup(of: Void.self, body: { taskGroup in + for user in userRecords { + taskGroup.addTask { + try? await DataSourceFacade.responseToUserViewButtonAction( + dependency: dependency, + user: user, + buttonState: .follow + ) + } + } + }) + + delegate?.homeTimelineNeedRefresh.send() + completion?() + } + } } diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift new file mode 100644 index 000000000..1b4fcb34c --- /dev/null +++ b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift @@ -0,0 +1,22 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Combine +import MastodonUI +import CoreDataStack + +extension SuggestionAccountTableViewCell { + final class ViewModel { + let user: MastodonUser + + let followedUsers: [String] + let blockedUsers: [String] + let followRequestedUsers: [String] + + init(user: MastodonUser, followedUsers: [String], blockedUsers: [String], followRequestedUsers: [String]) { + self.user = user + self.followedUsers = followedUsers + self.followRequestedUsers = followRequestedUsers + self.blockedUsers = blockedUsers + } + } +} diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift new file mode 100644 index 000000000..513bf63b6 --- /dev/null +++ b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift @@ -0,0 +1,115 @@ +// +// SuggestionAccountTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/21. +// + +import Combine +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import UIKit +import MetaTextKit +import MastodonMeta +import MastodonAsset +import MastodonLocalization +import MastodonUI +import MastodonCore + +protocol SuggestionAccountTableViewCellDelegate: AnyObject, UserViewDelegate {} + +final class SuggestionAccountTableViewCell: UITableViewCell { + + static let reuseIdentifier = "SuggestionAccountTableViewCell" + + var disposeBag = Set() + weak var delegate: SuggestionAccountTableViewCellDelegate? + + let userView: UserView + let bioMetaLabel: MetaLabel + private let contentStackView: UIStackView + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + + userView = UserView() + userView.translatesAutoresizingMaskIntoConstraints = false + + bioMetaLabel = MetaLabel() + bioMetaLabel.translatesAutoresizingMaskIntoConstraints = false + bioMetaLabel.numberOfLines = 0 + bioMetaLabel.textAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: UIColor.label + ] + bioMetaLabel.linkAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)), + .foregroundColor: Asset.Colors.brand.color + ] + bioMetaLabel.isUserInteractionEnabled = false + + contentStackView = UIStackView(arrangedSubviews: [userView, bioMetaLabel]) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.alignment = .leading + contentStackView.axis = .vertical + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(contentStackView) + + backgroundColor = .systemBackground + + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("We don't support ancient technology like Storyboards") } + + private func setupConstraints() { + let constraints = [ + contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + contentStackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 16), + + userView.widthAnchor.constraint(equalTo: contentStackView.widthAnchor), + bioMetaLabel.widthAnchor.constraint(equalTo: contentStackView.widthAnchor), + ] + + NSLayoutConstraint.activate(constraints) + } + + override func prepareForReuse() { + super.prepareForReuse() + + disposeBag.removeAll() + } + + func configure(viewModel: SuggestionAccountTableViewCell.ViewModel) { + userView.configure(user: viewModel.user, delegate: delegate) + + if viewModel.blockedUsers.contains(viewModel.user.id) { + self.userView.setButtonState(.blocked) + } else if viewModel.followedUsers.contains(viewModel.user.id) { + self.userView.setButtonState(.unfollow) + } else if viewModel.followRequestedUsers.contains(viewModel.user.id) { + self.userView.setButtonState(.pending) + } else if viewModel.user.locked { + self.userView.setButtonState(.request) + } else { + self.userView.setButtonState(.follow) + } + + let metaContent: MetaContent = { + do { + let mastodonContent = MastodonContent(content: viewModel.user.note ?? "", emojis: viewModel.user.emojis.asDictionary) + return try MastodonMetaContent.convert(document: mastodonContent) + } catch { + assertionFailure() + return PlaintextMetaContent(string: viewModel.user.note ?? "") + } + }() + + bioMetaLabel.configure(content: metaContent) + } +} diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewFooter.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewFooter.swift new file mode 100644 index 000000000..cf0408977 --- /dev/null +++ b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewFooter.swift @@ -0,0 +1,62 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonUI +import MastodonAsset +import MastodonLocalization + +protocol SuggestionAccountTableViewFooterDelegate: AnyObject { + func followAll(_ footerView: SuggestionAccountTableViewFooter) +} + +class SuggestionAccountTableViewFooter: UITableViewHeaderFooterView { + static let reuseIdentifier = "SuggestionAccountTableViewFooter" + + weak var delegate: SuggestionAccountTableViewFooterDelegate? + + let followAllButton: FollowButton + + override init(reuseIdentifier: String?) { + + //TODO: Check if we can use UIButton.configuration here instead? + followAllButton = FollowButton() + followAllButton.translatesAutoresizingMaskIntoConstraints = false + followAllButton.setTitle(L10n.Scene.SuggestionAccount.followAll, for: .normal) + followAllButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) + followAllButton.setTitleColor(.white, for: .normal) + followAllButton.contentEdgeInsets = .init(horizontal: 20, vertical: 12) + followAllButton.cornerRadius = 10 + followAllButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15)) + + followAllButton.setContentCompressionResistancePriority(.required, for: .horizontal) + followAllButton.setContentHuggingPriority(.required, for: .horizontal) + + super.init(reuseIdentifier: reuseIdentifier) + + contentView.addSubview(followAllButton) + setupConstraints() + + followAllButton.addTarget(self, action: #selector(SuggestionAccountTableViewFooter.followAll(_:)), for: .touchUpInside) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + followAllButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + followAllButton.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: followAllButton.trailingAnchor), + contentView.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: followAllButton.bottomAnchor, constant: 16), + + followAllButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96), + followAllButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 36), + ] + + NSLayoutConstraint.activate(constraints) + } + + //MARK: - Actions + @objc func followAll(_ sender: UIButton) { + delegate?.followAll(self) + } +} diff --git a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift deleted file mode 100644 index 6d93be1a9..000000000 --- a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// SuggestionAccountTableViewCell+ViewModel.swift -// Mastodon -// -// Created by MainasuK on 2022-2-16. -// - -import UIKit -import Combine -import CoreDataStack -import MastodonAsset -import MastodonCore -import MastodonUI -import MastodonMeta -import Meta - -extension SuggestionAccountTableViewCell { - - class ViewModel { - var disposeBag = Set() - - @Published public var userIdentifier: UserIdentifier? // me - - @Published var avatarImageURL: URL? - @Published public var authorName: MetaContent? - @Published public var authorUsername: String? - - @Published var isFollowing = false - @Published var isPending = false - - func prepareForReuse() { - isFollowing = false - isPending = false - } - } - -} - -extension SuggestionAccountTableViewCell.ViewModel { - func bind(cell: SuggestionAccountTableViewCell) { - // avatar - $avatarImageURL.removeDuplicates() - .sink { url in - let configuration = AvatarImageView.Configuration(url: url) - cell.avatarButton.avatarImageView.configure(configuration: configuration) - cell.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) - } - .store(in: &disposeBag) - // name - $authorName - .sink { metaContent in - let metaContent = metaContent ?? PlaintextMetaContent(string: " ") - cell.titleLabel.configure(content: metaContent) - } - .store(in: &disposeBag) - // username - $authorUsername - .map { text -> String in - guard let text = text else { return "" } - return "@\(text)" - } - .sink { username in - cell.subTitleLabel.text = username - } - .store(in: &disposeBag) - // button - Publishers.CombineLatest( - $isFollowing, - $isPending - ) - .sink { isFollowing, isPending in - let isFollowState = isFollowing || isPending - let imageName = isFollowState ? "minus.circle.fill" : "plus.circle" - let image = UIImage(systemName: imageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular)) - cell.button.setImage(image, for: .normal) - cell.button.tintColor = isFollowState ? Asset.Colors.danger.color : Asset.Colors.Label.secondary.color - } - .store(in: &disposeBag) - } -} - -extension SuggestionAccountTableViewCell { - func configure(user: MastodonUser) { - // author avatar - Publishers.CombineLatest( - user.publisher(for: \.avatar), - UserDefaults.shared.publisher(for: \.preferredStaticAvatar) - ) - .map { _ in user.avatarImageURL() } - .assign(to: \.avatarImageURL, on: viewModel) - .store(in: &disposeBag) - // author name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _, emojis in - do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure(error.localizedDescription) - return PlaintextMetaContent(string: user.displayNameWithFallback) - } - } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - // author username - user.publisher(for: \.acct) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) - // isFollowing - Publishers.CombineLatest( - viewModel.$userIdentifier, - user.publisher(for: \.followingBy) - ) - .map { userIdentifier, followingBy in - guard let userIdentifier = userIdentifier else { return false } - return followingBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isFollowing, on: viewModel) - .store(in: &disposeBag) - // isPending - Publishers.CombineLatest( - viewModel.$userIdentifier, - user.publisher(for: \.followRequestedBy) - ) - .map { userIdentifier, followRequestedBy in - guard let userIdentifier = userIdentifier else { return false } - return followRequestedBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isPending, on: viewModel) - .store(in: &disposeBag) - } -} diff --git a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift deleted file mode 100644 index d259ebed4..000000000 --- a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// SuggestionAccountTableViewCell.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/21. -// - -import os.log -import Combine -import CoreData -import CoreDataStack -import Foundation -import MastodonSDK -import UIKit -import MetaTextKit -import MastodonMeta -import MastodonAsset -import MastodonLocalization -import MastodonUI - -protocol SuggestionAccountTableViewCellDelegate: AnyObject { - func suggestionAccountTableViewCell(_ cell: SuggestionAccountTableViewCell, friendshipDidPressed button: UIButton) -} - -final class SuggestionAccountTableViewCell: UITableViewCell { - - let logger = Logger(subsystem: "SuggestionAccountTableViewCell", category: "View") - - var disposeBag = Set() - - weak var delegate: SuggestionAccountTableViewCellDelegate? - - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(cell: self) - return viewModel - }() - - let avatarButton = AvatarButton() - - let titleLabel = MetaLabel(style: .statusName) - - let subTitleLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.secondary.color - label.font = .preferredFont(forTextStyle: .body) - return label - }() - - let buttonContainer: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - let button: HighlightDimmableButton = { - let button = HighlightDimmableButton(type: .custom) - let image = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular)) - button.setImage(image, for: .normal) - return button - }() - - let activityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - return activityIndicatorView - }() - - override func prepareForReuse() { - super.prepareForReuse() - - disposeBag.removeAll() - avatarButton.avatarImageView.prepareForReuse() - viewModel.prepareForReuse() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - configure() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configure() - } - -} - -extension SuggestionAccountTableViewCell { - - private func configure() { - let containerStackView = UIStackView() - containerStackView.axis = .horizontal - containerStackView.distribution = .fill - containerStackView.spacing = 12 - containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 21, bottom: 12, right: 12) - containerStackView.isLayoutMarginsRelativeArrangement = true - containerStackView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(containerStackView) - containerStackView.pinToParent() - - avatarButton.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarButton.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1), - avatarButton.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), - ]) - - let textStackView = UIStackView() - textStackView.axis = .vertical - textStackView.distribution = .fill - textStackView.alignment = .leading - textStackView.translatesAutoresizingMaskIntoConstraints = false - titleLabel.translatesAutoresizingMaskIntoConstraints = false - textStackView.addArrangedSubview(titleLabel) - subTitleLabel.translatesAutoresizingMaskIntoConstraints = false - textStackView.addArrangedSubview(subTitleLabel) - subTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical) - - containerStackView.addArrangedSubview(textStackView) - textStackView.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) - - buttonContainer.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(buttonContainer) - NSLayoutConstraint.activate([ - buttonContainer.widthAnchor.constraint(equalToConstant: 24).priority(.required - 1), - buttonContainer.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), - ]) - buttonContainer.setContentHuggingPriority(.required - 1, for: .horizontal) - - activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - button.translatesAutoresizingMaskIntoConstraints = false - activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - buttonContainer.addSubview(button) - buttonContainer.addSubview(activityIndicatorView) - NSLayoutConstraint.activate([ - buttonContainer.centerXAnchor.constraint(equalTo: activityIndicatorView.centerXAnchor), - buttonContainer.centerYAnchor.constraint(equalTo: activityIndicatorView.centerYAnchor), - buttonContainer.centerXAnchor.constraint(equalTo: button.centerXAnchor), - buttonContainer.centerYAnchor.constraint(equalTo: button.centerYAnchor), - ]) - - button.addTarget(self, action: #selector(SuggestionAccountTableViewCell.buttonDidPressed(_:)), for: .touchUpInside) - } - -} - -extension SuggestionAccountTableViewCell { - @objc private func buttonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.suggestionAccountTableViewCell(self, friendshipDidPressed: sender) - } -} - -extension SuggestionAccountTableViewCell { - - func startAnimating() { - activityIndicatorView.isHidden = false - activityIndicatorView.startAnimating() - button.isHidden = true - } - - func stopAnimating() { - activityIndicatorView.stopAnimating() - activityIndicatorView.isHidden = true - button.isHidden = false - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift index 85dc666f2..e35264ed1 100644 --- a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -52,6 +52,7 @@ extension MastodonAuthenticationBox { public class MastodonAccountInMemoryCache { @Published public var followingUserIds: [String] = [] @Published public var blockedUserIds: [String] = [] + @Published public var followRequestedUserIDs: [String] = [] static var sharedCaches = [String: MastodonAccountInMemoryCache]() diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 27cc5a08d..2ba2ec5cb 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -83,11 +83,6 @@ final public class FeedFetchedResultsController: NSObject { } .store(in: &disposeBag) } - - deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - } - } // MARK: - NSFetchedResultsControllerDelegate @@ -96,7 +91,6 @@ extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate { _ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference ) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let snapshot = snapshot as NSDiffableDataSourceSnapshot self._objectIDs.send(snapshot.itemIdentifiers) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift index 82d814294..530d4288a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { #if DEBUG - private static let clientName = "Skimming" + private static let clientName = "Mastodon for iOS (Development)" #else private static let clientName = "Mastodon for iOS" #endif diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift index f0f0bfb7b..74650131d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift @@ -16,7 +16,7 @@ extension APIService { public func followRequest( userID: Mastodon.Entity.Account.ID, - query: Mastodon.API.Account.FollowReqeustQuery, + query: Mastodon.API.Account.FollowRequestQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { let response = try await Mastodon.API.Account.followRequest( @@ -51,4 +51,17 @@ extension APIService { return response } + public func pendingFollowRequest( + userID: Mastodon.Entity.Account.ID, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { + let response = try await Mastodon.API.Account.pendingFollowRequest( + session: session, + domain: authenticationBox.domain, + userID: userID, + authorization: authenticationBox.userAuthorization + ).singleOutput() + + return response + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 933fd1c6c..8faac74d9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -45,7 +45,12 @@ public final class AuthenticationService: NSObject { let blockedIds = try await apiService.getBlocked( authenticationBox: authBox ).value.map { $0.id } - + + let followRequestIds = try await apiService.pendingFollowRequest(userID: authBox.userID, + authenticationBox: authBox) + .value.map { $0.id } + + authBox.inMemoryCache.followRequestedUserIDs = followRequestIds authBox.inMemoryCache.followingUserIds = followingIds authBox.inMemoryCache.blockedUserIds = blockedIds } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index ef29f91d0..259af8fb9 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1484,10 +1484,10 @@ public enum L10n { } } public enum SuggestionAccount { - /// When you follow someone, you’ll see their posts in your home feed. - public static let followExplain = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowExplain", fallback: "When you follow someone, you’ll see their posts in your home feed.") - /// Find People to Follow - public static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title", fallback: "Find People to Follow") + /// Follow All + public static let followAll = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowAll", fallback: "Follow All") + /// Popular on Mastodon + public static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title", fallback: "Popular on Mastodon") } public enum Thread { /// Post diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 6120302c9..55cf7ec74 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -517,8 +517,8 @@ uploaded to Mastodon."; "Scene.Settings.Section.SpicyZone.Signout" = "Sign Out"; "Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone"; "Scene.Settings.Title" = "Settings"; -"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed."; -"Scene.SuggestionAccount.Title" = "Find People to Follow"; +"Scene.SuggestionAccount.Title" = "Popular on Mastodon"; +"Scene.SuggestionAccount.FollowAll" = "Follow All"; "Scene.Thread.BackTitle" = "Post"; "Scene.Thread.Title" = "Post from %@"; "Scene.Welcome.Education.A11Y.WhatIsMastodon.Title" = "What is Mastodon?"; @@ -552,4 +552,4 @@ uploaded to Mastodon."; "Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts."; "Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers"; "Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social"; -"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; \ No newline at end of file +"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 6120302c9..55cf7ec74 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -517,8 +517,8 @@ uploaded to Mastodon."; "Scene.Settings.Section.SpicyZone.Signout" = "Sign Out"; "Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone"; "Scene.Settings.Title" = "Settings"; -"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed."; -"Scene.SuggestionAccount.Title" = "Find People to Follow"; +"Scene.SuggestionAccount.Title" = "Popular on Mastodon"; +"Scene.SuggestionAccount.FollowAll" = "Follow All"; "Scene.Thread.BackTitle" = "Post"; "Scene.Thread.Title" = "Post from %@"; "Scene.Welcome.Education.A11Y.WhatIsMastodon.Title" = "What is Mastodon?"; @@ -552,4 +552,4 @@ uploaded to Mastodon."; "Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts."; "Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers"; "Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social"; -"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; \ No newline at end of file +"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift index ddde4ffdc..fdf091c86 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -10,6 +10,11 @@ import Combine // MARK: - Account credentials extension Mastodon.API.Account { + + static func pendingFollowRequestEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain) + .appendingPathComponent("follow_requests") + } static func acceptFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL { return Mastodon.API.endpointURL(domain: domain) @@ -25,7 +30,7 @@ extension Mastodon.API.Account { .appendingPathComponent("reject") } - /// Accept Follow + /// Pending Follow Requests /// /// /// - Since: 0.0.0 @@ -37,6 +42,38 @@ extension Mastodon.API.Account { /// - domain: Mastodon instance domain. e.g. "example.com" /// - userID: ID of the account in the database /// - authorization: User token + /// - Returns: `AnyPublisher` contains `[Account]` nested in the response + public static func pendingFollowRequest( + session: URLSession, + domain: String, + userID: Mastodon.Entity.Account.ID, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: pendingFollowRequestEndpointURL(domain: domain), + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + + /// Accept Follow + /// + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/#allow) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - userID: ID of the account in the database + /// - authorization: User token /// - Returns: `AnyPublisher` contains `Relationship` nested in the response public static func acceptFollowRequest( session: URLSession, @@ -63,7 +100,7 @@ extension Mastodon.API.Account { /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Reference - /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/) + /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/#reject) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" @@ -92,7 +129,7 @@ extension Mastodon.API.Account { extension Mastodon.API.Account { - public enum FollowReqeustQuery { + public enum FollowRequestQuery { case accept case reject } @@ -101,7 +138,7 @@ extension Mastodon.API.Account { session: URLSession, domain: String, userID: Mastodon.Entity.Account.ID, - query: FollowReqeustQuery, + query: FollowRequestQuery, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { switch query { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift new file mode 100644 index 000000000..ff82aa3cb --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift @@ -0,0 +1,31 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonAsset + +public final class FollowButton: RoundedEdgesButton { + + public init() { + super.init(frame: .zero) + configureAppearance() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureAppearance() { + setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) + setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted) + switch traitCollection.userInterfaceStyle { + case .dark: + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal) + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted) + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled) + default: + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal) + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted) + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index b02fd3602..28125d304 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -20,7 +20,7 @@ public protocol UserViewDelegate: AnyObject { public final class UserView: UIView { public enum ButtonState { - case none, loading, follow, unfollow, blocked + case none, loading, follow, request, pending, unfollow, blocked } private var currentButtonState: ButtonState = .none @@ -99,7 +99,8 @@ public final class UserView: UIView { label.textColor = .secondaryLabel return label }() - + + private let followButtonWrapper = UIView() private let followButton: FollowButton = { let button = FollowButton() button.cornerRadius = 10 @@ -149,10 +150,7 @@ extension UserView { avatarButton.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarButton.widthAnchor.constraint(equalToConstant: 28).priority(.required - 1), - avatarButton.heightAnchor.constraint(equalToConstant: 28).priority(.required - 1), - ]) + avatarButton.setContentHuggingPriority(.defaultLow, for: .vertical) avatarButton.setContentHuggingPriority(.defaultLow, for: .horizontal) @@ -162,7 +160,19 @@ extension UserView { containerStackView.addArrangedSubview(labelStackView) // follow button - containerStackView.addArrangedSubview(followButton) + followButtonWrapper.translatesAutoresizingMaskIntoConstraints = false + followButtonWrapper.addSubview(followButton) + + containerStackView.addArrangedSubview(followButtonWrapper) + + NSLayoutConstraint.activate([ + followButton.topAnchor.constraint(lessThanOrEqualTo: avatarButton.topAnchor), + followButton.leadingAnchor.constraint(equalTo: followButtonWrapper.leadingAnchor), + followButtonWrapper.trailingAnchor.constraint(equalTo: followButton.trailingAnchor), + followButtonWrapper.bottomAnchor.constraint(greaterThanOrEqualTo: followButton.bottomAnchor), + + followButtonWrapper.heightAnchor.constraint(equalTo: containerStackView.heightAnchor), + ]) let nameStackView = UIStackView() nameStackView.axis = .horizontal @@ -181,7 +191,14 @@ extension UserView { authorUsernameLabel.setContentCompressionResistancePriority(.defaultHigh - 1, for: .horizontal) labelStackView.addArrangedSubview(nameStackView) - + + NSLayoutConstraint.activate([ + avatarButton.heightAnchor.constraint(lessThanOrEqualToConstant: 56), + avatarButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 28), + avatarButton.heightAnchor.constraint(equalTo: avatarButton.widthAnchor), + avatarButton.heightAnchor.constraint(equalTo: labelStackView.heightAnchor), + ]) + let verifiedSpacerView = UIView() let verifiedStackTrailingSpacerView = UIView() @@ -216,33 +233,6 @@ extension UserView { } -private final class FollowButton: RoundedEdgesButton { - - init() { - super.init(frame: .zero) - configureAppearance() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func configureAppearance() { - setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) - setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted) - switch traitCollection.userInterfaceStyle { - case .dark: - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled) - default: - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled) - } - } -} - public extension UserView { private func prepareButtonStateLayout(for state: ButtonState) { switch state { @@ -269,6 +259,7 @@ public extension UserView { prepareButtonStateLayout(for: state) switch state { + case .loading: followButton.isHidden = false followButton.setTitle(nil, for: .normal) @@ -279,7 +270,19 @@ public extension UserView { followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal) followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) followButton.setTitleColor(.white, for: .normal) - + + case .request: + followButton.isHidden = false + followButton.setTitle(L10n.Common.Controls.Friendship.request, for: .normal) + followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) + followButton.setTitleColor(.white, for: .normal) + + case .pending: + followButton.isHidden = false + followButton.setTitle(L10n.Common.Controls.Friendship.pending, for: .normal) + followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal) + followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal) + case .unfollow: followButton.isHidden = false followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)