Merge pull request #1044 from mastodon/ios-157-popular-on-mastodon
Better UI/UX for suggestions for new users (IOS-157)
This commit is contained in:
commit
ddf0afcc6d
|
@ -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": {
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; };
|
||||
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||
2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedAccountSection.swift; sourceTree = "<group>"; };
|
||||
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedAccountItem.swift; sourceTree = "<group>"; };
|
||||
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = "<group>"; };
|
||||
2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = "<group>"; };
|
||||
2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -801,6 +795,8 @@
|
|||
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = "<group>"; };
|
||||
D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = "<group>"; };
|
||||
|
@ -1135,7 +1131,6 @@
|
|||
DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = "<group>"; };
|
||||
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountItem.swift; sourceTree = "<group>"; };
|
||||
DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1168,7 +1163,6 @@
|
|||
DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = "<group>"; };
|
||||
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; };
|
||||
DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+TableViewControllerNavigateable.swift"; sourceTree = "<group>"; };
|
||||
DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+StatusTableViewControllerNavigateable.swift"; sourceTree = "<group>"; };
|
||||
DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPostsViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1576,14 +1570,6 @@
|
|||
path = Button;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D4AD89A2631659400613EFC /* CollectionViewCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */,
|
||||
);
|
||||
path = CollectionViewCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
2DE0FAC62615F5D200CDF649 /* View */ = {
|
||||
|
@ -1907,15 +1892,6 @@
|
|||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0617FB27855B740030EE79 /* Account */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
|
||||
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
|
||||
);
|
||||
path = Account;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0618082785B2790030EE79 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1943,15 +1919,6 @@
|
|||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0FCB892796BE1E006C02E2 /* RecommandAccount */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */,
|
||||
);
|
||||
path = RecommandAccount;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
@ -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<MastodonUser>)
|
||||
case placeHolder(uuid: UUID)
|
||||
}
|
|
@ -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<SelectedAccountSection, SelectedAccountItem> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<RecommendAccountSection, NSManagedObjectID> {
|
||||
// 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<RecommendAccountSection, RecommendAccountItem> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ extension DataSourceFacade {
|
|||
static func responseToUserFollowRequestAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
notification: ManagedObjectRecord<Notification>,
|
||||
query: Mastodon.API.Account.FollowReqeustQuery
|
||||
query: Mastodon.API.Account.FollowRequestQuery
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -30,7 +30,8 @@ final class HomeTimelineViewModel: NSObject {
|
|||
let fetchedResultsController: FeedFetchedResultsController
|
||||
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
var presentedSuggestions = false
|
||||
|
||||
@Published var lastAutomaticFetchTimestamp: Date? = nil
|
||||
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<RecommendAccountSection, RecommendAccountItem> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
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 { }
|
||||
|
|
|
@ -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<RecommendAccountSection, RecommendAccountItem>()
|
||||
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<SelectedAccountSection, SelectedAccountItem>()
|
||||
snapshot.appendSections([.main])
|
||||
var items: [SelectedAccountItem] = records.map { SelectedAccountItem.account($0) }
|
||||
|
||||
if items.count < 10 {
|
||||
let count = 10 - items.count
|
||||
let placeholderItems: [SelectedAccountItem] = (0..<count).map { _ in
|
||||
SelectedAccountItem.placeHolder(uuid: UUID())
|
||||
}
|
||||
items.append(contentsOf: placeholderItems)
|
||||
}
|
||||
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
collectionViewDiffableDataSource.applySnapshotUsingReloadData(snapshot, completion: nil)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
|
@ -11,7 +11,6 @@ import CoreDataStack
|
|||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
protocol SuggestionAccountViewModelDelegate: AnyObject {
|
||||
|
@ -27,12 +26,10 @@ final class SuggestionAccountViewModel: NSObject {
|
|||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
let selectedUserFetchedResultsController: UserFetchedResultsController
|
||||
|
||||
|
||||
var viewWillAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var collectionViewDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>?
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem>?
|
||||
|
||||
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<RecommendAccountSection, RecommendAccountItem>()
|
||||
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?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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]()
|
||||
|
||||
|
|
|
@ -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<NSFetchRequestResult>,
|
||||
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
|
||||
) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
||||
self._objectIDs.send(snapshot.itemIdentifiers)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Mastodon.Entity.Relationship> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
|
|
|
@ -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";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
|
|
|
@ -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<Mastodon.Response.Content<[Mastodon.Entity.Account]>, 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<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
switch query {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue