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": {
|
"suggestion_account": {
|
||||||
"title": "Find People to Follow",
|
"title": "Popular on Mastodon",
|
||||||
"follow_explain": "When you follow someone, you’ll see their posts in your home feed."
|
"follow_all": "Follow all"
|
||||||
},
|
},
|
||||||
"compose": {
|
"compose": {
|
||||||
"title": {
|
"title": {
|
||||||
|
|
|
@ -75,9 +75,6 @@
|
||||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; };
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; };
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
|
||||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.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 */; };
|
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; };
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; };
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; };
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.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 */; };
|
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
|
||||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
||||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.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 */; };
|
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 */; };
|
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; };
|
||||||
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.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 */; };
|
DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */; };
|
||||||
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; };
|
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; };
|
||||||
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.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 */; };
|
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
|
||||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; };
|
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; };
|
||||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.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 */; };
|
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; };
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; };
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; };
|
||||||
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.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 */; };
|
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 */; };
|
DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */; };
|
||||||
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPostsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1576,14 +1570,6 @@
|
||||||
path = Button;
|
path = Button;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
2D4AD89A2631659400613EFC /* CollectionViewCell */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */,
|
|
||||||
);
|
|
||||||
path = CollectionViewCell;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
2D59819925E4A55C000FB903 /* ConfirmEmail */ = {
|
2D59819925E4A55C000FB903 /* ConfirmEmail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1625,10 +1611,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB4F097826A039B400D62E92 /* Onboarding */,
|
DB4F097826A039B400D62E92 /* Onboarding */,
|
||||||
DB0617FB27855B740030EE79 /* Account */,
|
|
||||||
DB0617F827855B170030EE79 /* User */,
|
DB0617F827855B170030EE79 /* User */,
|
||||||
DB0617F927855B460030EE79 /* Profile */,
|
DB0617F927855B460030EE79 /* Profile */,
|
||||||
DB0FCB892796BE1E006C02E2 /* RecommandAccount */,
|
|
||||||
DB4F097926A039C400D62E92 /* Status */,
|
DB4F097926A039C400D62E92 /* Status */,
|
||||||
DB65C63527A2AF52008BAC2E /* Report */,
|
DB65C63527A2AF52008BAC2E /* Report */,
|
||||||
DB0617F727855B010030EE79 /* Notification */,
|
DB0617F727855B010030EE79 /* Notification */,
|
||||||
|
@ -1683,22 +1667,23 @@
|
||||||
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = {
|
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||||
|
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */,
|
||||||
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */,
|
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */,
|
||||||
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */,
|
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */,
|
||||||
DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */,
|
2DAC9E43262FC9DE0062E1A6 /* TableView-Components */,
|
||||||
2D4AD89A2631659400613EFC /* CollectionViewCell */,
|
|
||||||
2DAC9E43262FC9DE0062E1A6 /* TableViewCell */,
|
|
||||||
);
|
);
|
||||||
path = SuggestionAccount;
|
path = SuggestionAccount;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
2DAC9E43262FC9DE0062E1A6 /* TableViewCell */ = {
|
2DAC9E43262FC9DE0062E1A6 /* TableView-Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */,
|
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */,
|
||||||
DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */,
|
D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */,
|
||||||
|
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */,
|
||||||
);
|
);
|
||||||
path = TableViewCell;
|
path = "TableView-Components";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
2DE0FAC62615F5D200CDF649 /* View */ = {
|
2DE0FAC62615F5D200CDF649 /* View */ = {
|
||||||
|
@ -1907,15 +1892,6 @@
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB0617FB27855B740030EE79 /* Account */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
|
|
||||||
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
|
|
||||||
);
|
|
||||||
path = Account;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
DB0618082785B2790030EE79 /* Cell */ = {
|
DB0618082785B2790030EE79 /* Cell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1943,15 +1919,6 @@
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB0FCB892796BE1E006C02E2 /* RecommandAccount */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
|
||||||
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */,
|
|
||||||
);
|
|
||||||
path = RecommandAccount;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
DB1D187125EF5BBD003F1F23 /* TableView */ = {
|
DB1D187125EF5BBD003F1F23 /* TableView */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -3563,6 +3530,7 @@
|
||||||
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
||||||
62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */,
|
62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */,
|
||||||
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
||||||
|
D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */,
|
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */,
|
||||||
DB5B54B22833C24B00DEF8B2 /* RebloggedByViewController+DataSourceProvider.swift in Sources */,
|
DB5B54B22833C24B00DEF8B2 /* RebloggedByViewController+DataSourceProvider.swift in Sources */,
|
||||||
|
@ -3618,7 +3586,6 @@
|
||||||
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
||||||
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
|
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
|
||||||
62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */,
|
62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */,
|
||||||
DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */,
|
|
||||||
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */,
|
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */,
|
||||||
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
||||||
DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */,
|
DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */,
|
||||||
|
@ -3632,7 +3599,6 @@
|
||||||
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
|
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
|
||||||
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
|
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
|
||||||
DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */,
|
DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */,
|
||||||
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */,
|
|
||||||
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
|
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
|
||||||
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */,
|
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */,
|
||||||
DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */,
|
DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */,
|
||||||
|
@ -3689,7 +3655,6 @@
|
||||||
DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */,
|
DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */,
|
||||||
DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */,
|
DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */,
|
||||||
DBF1572F27046F1A00EC00B7 /* SecondaryPlaceholderViewController.swift in Sources */,
|
DBF1572F27046F1A00EC00B7 /* SecondaryPlaceholderViewController.swift in Sources */,
|
||||||
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
|
|
||||||
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
||||||
2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */,
|
2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */,
|
||||||
DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */,
|
DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */,
|
||||||
|
@ -3856,7 +3821,6 @@
|
||||||
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
|
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
|
||||||
DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */,
|
DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */,
|
||||||
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
|
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
|
||||||
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */,
|
|
||||||
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,
|
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,
|
||||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||||
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */,
|
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */,
|
||||||
|
@ -3888,6 +3852,7 @@
|
||||||
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
|
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
|
||||||
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
|
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
|
||||||
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
|
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
|
||||||
|
D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */,
|
||||||
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */,
|
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */,
|
||||||
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */,
|
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */,
|
||||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
||||||
|
@ -3927,7 +3892,6 @@
|
||||||
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||||
DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */,
|
DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */,
|
|
||||||
DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */,
|
DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */,
|
||||||
85BC11B32932414900E191CD /* AltTextViewController.swift in Sources */,
|
85BC11B32932414900E191CD /* AltTextViewController.swift in Sources */,
|
||||||
DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.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 }
|
guard let user = item.object(in: context.managedObjectContext) else { return }
|
||||||
cell.configure(
|
cell.configure(
|
||||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user,
|
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
|
delegate: configuration.searchHistorySectionHeaderCollectionReusableViewDelegate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,10 @@ extension SearchResultSection {
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
cell: cell,
|
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
|
configuration: configuration
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,11 @@ extension UserSection {
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
cell: cell,
|
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
|
configuration: configuration
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ extension DataSourceFacade {
|
||||||
static func responseToUserFollowRequestAction(
|
static func responseToUserFollowRequestAction(
|
||||||
dependency: NeedsDependency & AuthContextProvider,
|
dependency: NeedsDependency & AuthContextProvider,
|
||||||
notification: ManagedObjectRecord<Notification>,
|
notification: ManagedObjectRecord<Notification>,
|
||||||
query: Mastodon.API.Account.FollowReqeustQuery
|
query: Mastodon.API.Account.FollowRequestQuery
|
||||||
) async throws {
|
) async throws {
|
||||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||||
await selectionFeedbackGenerator.selectionChanged()
|
await selectionFeedbackGenerator.selectionChanged()
|
||||||
|
|
|
@ -22,6 +22,17 @@ extension DataSourceFacade {
|
||||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(userObject.id)
|
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:
|
case .unfollow:
|
||||||
try await DataSourceFacade.responseToUserFollowAction(
|
try await DataSourceFacade.responseToUserFollowAction(
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
|
@ -39,6 +50,16 @@ extension DataSourceFacade {
|
||||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(userObject.id)
|
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:
|
case .none, .loading:
|
||||||
break //no-op
|
break //no-op
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,6 +240,19 @@ extension HomeTimelineViewController {
|
||||||
.sink { [weak self] isEmpty in
|
.sink { [weak self] isEmpty in
|
||||||
if isEmpty {
|
if isEmpty {
|
||||||
self?.showEmptyView()
|
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 {
|
} else {
|
||||||
self?.emptyView.removeFromSuperview()
|
self?.emptyView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
@ -285,8 +298,6 @@ extension HomeTimelineViewController {
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
viewModel.viewDidAppear.send()
|
|
||||||
|
|
||||||
if let timestamp = viewModel.lastAutomaticFetchTimestamp {
|
if let timestamp = viewModel.lastAutomaticFetchTimestamp {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if now.timeIntervalSince(timestamp) > 60 {
|
if now.timeIntervalSince(timestamp) > 60 {
|
||||||
|
@ -360,6 +371,7 @@ extension HomeTimelineViewController {
|
||||||
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
|
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 0.8),
|
||||||
|
manuallySearchButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
|
||||||
])
|
])
|
||||||
|
|
||||||
let buttonContainerStackView = UIStackView()
|
let buttonContainerStackView = UIStackView()
|
||||||
|
@ -374,9 +386,10 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - Actions
|
||||||
extension HomeTimelineViewController {
|
extension HomeTimelineViewController {
|
||||||
|
|
||||||
@objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) {
|
@objc private func findPeopleButtonPressed(_ sender: Any?) {
|
||||||
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext)
|
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext)
|
||||||
suggestionAccountViewModel.delegate = viewModel
|
suggestionAccountViewModel.delegate = viewModel
|
||||||
_ = coordinator.present(
|
_ = coordinator.present(
|
||||||
|
@ -387,13 +400,11 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func manuallySearchButtonPressed(_ sender: UIButton) {
|
@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)
|
let searchDetailViewModel = SearchDetailViewModel(authContext: viewModel.authContext)
|
||||||
_ = coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
_ = coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@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 }
|
guard let setting = context.settingService.currentSetting.value else { return }
|
||||||
let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
|
let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
|
||||||
_ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
_ = 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 fetchedResultsController: FeedFetchedResultsController
|
||||||
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
|
||||||
|
var presentedSuggestions = false
|
||||||
|
|
||||||
@Published var lastAutomaticFetchTimestamp: Date? = nil
|
@Published var lastAutomaticFetchTimestamp: Date? = nil
|
||||||
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
|
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
|
||||||
|
|
|
@ -301,7 +301,7 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
|
||||||
|
|
||||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
OperationQueue.main.addOperation {
|
DispatchQueue.main.async {
|
||||||
let numberOfResults = viewModel.filteredServers.count
|
let numberOfResults = viewModel.filteredServers.count
|
||||||
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ extension PickServerSection {
|
||||||
}()
|
}()
|
||||||
if let proxiedThumbnail = server.proxiedThumbnail, let thumbnailUrl = URL(string: proxiedThumbnail) {
|
if let proxiedThumbnail = server.proxiedThumbnail, let thumbnailUrl = URL(string: proxiedThumbnail) {
|
||||||
cell.thumbnailImageView.af.setImage(withURL: thumbnailUrl, completion: { _ in
|
cell.thumbnailImageView.af.setImage(withURL: thumbnailUrl, completion: { _ in
|
||||||
OperationQueue.main.addOperation {
|
DispatchQueue.main.async {
|
||||||
cell.thumbnailImageView.isHidden = false
|
cell.thumbnailImageView.isHidden = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -280,8 +280,6 @@ extension ProfileHeaderViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
|
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
|
// set title view offset
|
||||||
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
|
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
|
||||||
let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
|
let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
|
||||||
|
|
|
@ -289,6 +289,12 @@ extension ProfileViewController {
|
||||||
bindPager()
|
bindPager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
navigationController?.navigationBar.prefersLargeTitles = false
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
@ -374,7 +380,7 @@ extension ProfileViewController {
|
||||||
profileHeaderViewController.profileHeaderView.viewModel.$name
|
profileHeaderViewController.profileHeaderView.viewModel.$name
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] name in
|
.sink { [weak self] name in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.isModal == false else { return }
|
||||||
self.navigationItem.title = name
|
self.navigationItem.title = name
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -422,7 +428,7 @@ extension ProfileViewController {
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] menu in
|
} receiveValue: { [weak self] menu in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
OperationQueue.main.addOperation {
|
DispatchQueue.main.async {
|
||||||
self.moreMenuBarButtonItem.menu = menu
|
self.moreMenuBarButtonItem.menu = menu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,12 @@ extension SearchHistoryUserCollectionViewCell {
|
||||||
|
|
||||||
let followedUsers: AnyPublisher<[String], Never>
|
let followedUsers: AnyPublisher<[String], Never>
|
||||||
let blockedUsers: AnyPublisher<[String], Never>
|
let blockedUsers: AnyPublisher<[String], Never>
|
||||||
|
let followRequestedUsers: AnyPublisher<[String], Never>
|
||||||
init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>) {
|
|
||||||
|
init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
|
||||||
self.value = value
|
self.value = value
|
||||||
self.followedUsers = followedUsers
|
self.followedUsers = followedUsers
|
||||||
|
self.followRequestedUsers = followRequestedUsers
|
||||||
self.blockedUsers = blockedUsers
|
self.blockedUsers = blockedUsers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,25 +47,26 @@ extension SearchHistoryUserCollectionViewCell {
|
||||||
userView.setButtonState(.loading)
|
userView.setButtonState(.loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest3(
|
||||||
viewModel.followedUsers,
|
viewModel.followedUsers,
|
||||||
|
viewModel.followRequestedUsers,
|
||||||
viewModel.blockedUsers
|
viewModel.blockedUsers
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] followed, blocked in
|
.sink { [weak self] followed, requested, blocked in
|
||||||
if blocked.contains(where: { $0 == user.id }) {
|
if blocked.contains(user.id) {
|
||||||
self?.userView.setButtonState(.blocked)
|
self?.userView.setButtonState(.blocked)
|
||||||
} else if followed.contains(where: { $0 == user.id }) {
|
} else if followed.contains(user.id) {
|
||||||
self?.userView.setButtonState(.unfollow)
|
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?.userView.setButtonState(.follow)
|
||||||
}
|
}
|
||||||
|
|
||||||
self?.setNeedsLayout()
|
|
||||||
self?.setNeedsUpdateConstraints()
|
|
||||||
self?.layoutIfNeeded()
|
|
||||||
}
|
}
|
||||||
.store(in: &_disposeBag)
|
.store(in: &_disposeBag)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ extension StatusTableViewCell {
|
||||||
if statusView.frame == .zero {
|
if statusView.frame == .zero {
|
||||||
// set status view width
|
// set status view width
|
||||||
statusView.frame.size.width = tableView.frame.width - containerViewHorizontalMargin
|
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 {
|
switch viewModel.value {
|
||||||
|
|
|
@ -15,8 +15,6 @@ import MastodonUI
|
||||||
final class StatusTableViewCell: UITableViewCell {
|
final class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
static let marginForRegularHorizontalSizeClass: CGFloat = 64
|
static let marginForRegularHorizontalSizeClass: CGFloat = 64
|
||||||
|
|
||||||
let logger = Logger(subsystem: "StatusTableViewCell", category: "View")
|
|
||||||
|
|
||||||
weak var delegate: StatusTableViewCellDelegate?
|
weak var delegate: StatusTableViewCellDelegate?
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
@ -44,11 +42,6 @@ final class StatusTableViewCell: UITableViewCell {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewCell {
|
extension StatusTableViewCell {
|
||||||
|
|
|
@ -16,10 +16,12 @@ extension UserTableViewCell {
|
||||||
|
|
||||||
let followedUsers: AnyPublisher<[String], Never>
|
let followedUsers: AnyPublisher<[String], Never>
|
||||||
let blockedUsers: 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.value = value
|
||||||
self.followedUsers = followedUsers
|
self.followedUsers = followedUsers
|
||||||
|
self.followRequestedUsers = followRequestedUsers
|
||||||
self.blockedUsers = blockedUsers
|
self.blockedUsers = blockedUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,20 +54,24 @@ extension UserTableViewCell {
|
||||||
userView.setButtonState(.loading)
|
userView.setButtonState(.loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest3(
|
||||||
viewModel.followedUsers,
|
viewModel.followedUsers,
|
||||||
|
viewModel.followRequestedUsers,
|
||||||
viewModel.blockedUsers
|
viewModel.blockedUsers
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] followed, blocked in
|
.sink { [weak self] followed, requested, blocked in
|
||||||
if blocked.contains(user.id) {
|
if blocked.contains(user.id) {
|
||||||
self?.userView.setButtonState(.blocked)
|
self?.userView.setButtonState(.blocked)
|
||||||
} else if followed.contains(user.id) {
|
} else if followed.contains(user.id) {
|
||||||
self?.userView.setButtonState(.unfollow)
|
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 {
|
} else if user != me {
|
||||||
self?.userView.setButtonState(.follow)
|
self?.userView.setButtonState(.follow)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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 CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
|
@ -17,71 +16,28 @@ import MastodonUI
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
class SuggestionAccountViewController: UIViewController, NeedsDependency {
|
class SuggestionAccountViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
static let collectionViewHeight: CGFloat = 24 + 64 + 24
|
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var viewModel: SuggestionAccountViewModel!
|
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: UITableView = {
|
||||||
let tableView = ControlContainableTableView()
|
let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||||
tableView.register(SuggestionAccountTableViewCell.self, forCellReuseIdentifier: String(describing: SuggestionAccountTableViewCell.self))
|
tableView.register(SuggestionAccountTableViewCell.self, forCellReuseIdentifier: SuggestionAccountTableViewCell.reuseIdentifier)
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
// we're lazy, that's why we don't put the Footer in tableViewFooter
|
||||||
tableView.tableFooterView = UIView()
|
tableView.register(SuggestionAccountTableViewFooter.self, forHeaderFooterViewReuseIdentifier: SuggestionAccountTableViewFooter.reuseIdentifier)
|
||||||
tableView.separatorStyle = .singleLine
|
tableView.contentInset.top = 16
|
||||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
||||||
return tableView
|
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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
setupNavigationBarAppearance()
|
||||||
ThemeService.shared.currentTheme
|
defer { setupNavigationBarBackgroundView() }
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] theme in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.setupBackgroundColor(theme: theme)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
title = L10n.Scene.SuggestionAccount.title
|
title = L10n.Scene.SuggestionAccount.title
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
|
@ -89,66 +45,40 @@ extension SuggestionAccountViewController {
|
||||||
target: self,
|
target: self,
|
||||||
action: #selector(SuggestionAccountViewController.doneButtonDidClick(_:))
|
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
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(tableView)
|
view.addSubview(tableView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
tableView.topAnchor.constraint(equalTo: collectionView.bottomAnchor),
|
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
collectionView.delegate = self
|
|
||||||
viewModel.setupDiffableDataSource(
|
|
||||||
collectionView: collectionView
|
|
||||||
)
|
|
||||||
|
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
suggestionAccountTableViewCellDelegate: self
|
suggestionAccountTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
|
|
||||||
|
view.backgroundColor = .secondarySystemBackground
|
||||||
|
tableView.backgroundColor = .secondarySystemBackground
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
navigationController?.navigationBar.prefersLargeTitles = true
|
||||||
|
navigationItem.largeTitleDisplayMode = .automatic
|
||||||
|
|
||||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupBackgroundColor(theme: Theme) {
|
//MARK: - Actions
|
||||||
view.backgroundColor = theme.systemBackgroundColor
|
|
||||||
collectionView.backgroundColor = theme.systemGroupedBackgroundColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UICollectionViewDelegateFlowLayout
|
@objc func doneButtonDidClick(_ sender: UIButton) {
|
||||||
extension SuggestionAccountViewController: UICollectionViewDelegate {
|
viewModel.delegate?.homeTimelineNeedRefresh.send()
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
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
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// MARK: - AuthContextProvider
|
||||||
|
@ -175,39 +114,21 @@ extension SuggestionAccountViewController: AuthContextProvider {
|
||||||
var authContext: AuthContext { viewModel.authContext }
|
var authContext: AuthContext { viewModel.authContext }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UserTableViewCellDelegate
|
||||||
|
extension SuggestionAccountViewController: UserTableViewCellDelegate {}
|
||||||
|
|
||||||
// MARK: - SuggestionAccountTableViewCellDelegate
|
// MARK: - SuggestionAccountTableViewCellDelegate
|
||||||
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate {
|
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate { }
|
||||||
func suggestionAccountTableViewCell(
|
|
||||||
_ cell: SuggestionAccountTableViewCell,
|
|
||||||
friendshipDidPressed button: UIButton
|
extension SuggestionAccountViewController: SuggestionAccountTableViewFooterDelegate {
|
||||||
) {
|
func followAll(_ footerView: SuggestionAccountTableViewFooter) {
|
||||||
guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
|
viewModel.followAllSuggestedAccounts(self) {
|
||||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
DispatchQueue.main.async {
|
||||||
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
self.dismiss(animated: true)
|
||||||
|
}
|
||||||
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 {
|
extension SuggestionAccountViewController: OnboardingViewControllerAppearance { }
|
||||||
@objc func doneButtonDidClick(_ sender: UIButton) {
|
|
||||||
dismiss(animated: true, completion: nil)
|
|
||||||
// if viewModel.selectedAccounts.value.count > 0 {
|
|
||||||
// viewModel.delegate?.homeTimelineNeedRefresh.send()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import os.log
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol SuggestionAccountViewModelDelegate: AnyObject {
|
protocol SuggestionAccountViewModelDelegate: AnyObject {
|
||||||
|
@ -27,12 +26,10 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
let userFetchedResultsController: UserFetchedResultsController
|
||||||
let selectedUserFetchedResultsController: UserFetchedResultsController
|
|
||||||
|
|
||||||
var viewWillAppear = PassthroughSubject<Void, Never>()
|
var viewWillAppear = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var collectionViewDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>?
|
|
||||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem>?
|
var tableViewDiffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem>?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
@ -46,26 +43,16 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
domain: nil,
|
domain: nil,
|
||||||
additionalPredicate: nil
|
additionalPredicate: nil
|
||||||
)
|
)
|
||||||
self.selectedUserFetchedResultsController = UserFetchedResultsController(
|
|
||||||
managedObjectContext: context.managedObjectContext,
|
|
||||||
domain: nil,
|
|
||||||
additionalPredicate: nil
|
|
||||||
)
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain
|
userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain
|
||||||
selectedUserFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain
|
|
||||||
selectedUserFetchedResultsController.additionalPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [
|
// fetch recommended users
|
||||||
MastodonUser.predicate(followingBy: authContext.mastodonAuthenticationBox.userID),
|
|
||||||
MastodonUser.predicate(followRequestedBy: authContext.mastodonAuthenticationBox.userID)
|
|
||||||
])
|
|
||||||
|
|
||||||
// fetch recomment users
|
|
||||||
Task {
|
Task {
|
||||||
var userIDs: [MastodonUser.ID] = []
|
var userIDs: [MastodonUser.ID] = []
|
||||||
do {
|
do {
|
||||||
let response = try await context.apiService.suggestionAccountV2(
|
let response = try await context.apiService.suggestionAccountV2(
|
||||||
query: nil,
|
query: .init(limit: 5),
|
||||||
authenticationBox: authContext.mastodonAuthenticationBox
|
authenticationBox: authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
userIDs = response.value.map { $0.account.id }
|
userIDs = response.value.map { $0.account.id }
|
||||||
|
@ -76,12 +63,11 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
)
|
)
|
||||||
userIDs = response.value.map { $0.id }
|
userIDs = response.value.map { $0.id }
|
||||||
} catch {
|
} 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 }
|
guard !userIDs.isEmpty else { return }
|
||||||
userFetchedResultsController.userIDs = userIDs
|
userFetchedResultsController.userIDs = userIDs
|
||||||
selectedUserFetchedResultsController.userIDs = userIDs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch relationship
|
// fetch relationship
|
||||||
|
@ -99,4 +85,56 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
.store(in: &disposeBag)
|
.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 {
|
public class MastodonAccountInMemoryCache {
|
||||||
@Published public var followingUserIds: [String] = []
|
@Published public var followingUserIds: [String] = []
|
||||||
@Published public var blockedUserIds: [String] = []
|
@Published public var blockedUserIds: [String] = []
|
||||||
|
@Published public var followRequestedUserIDs: [String] = []
|
||||||
|
|
||||||
static var sharedCaches = [String: MastodonAccountInMemoryCache]()
|
static var sharedCaches = [String: MastodonAccountInMemoryCache]()
|
||||||
|
|
||||||
|
|
|
@ -83,11 +83,6 @@ final public class FeedFetchedResultsController: NSObject {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - NSFetchedResultsControllerDelegate
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
@ -96,7 +91,6 @@ extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
||||||
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
|
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
|
||||||
) {
|
) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
||||||
self._objectIDs.send(snapshot.itemIdentifiers)
|
self._objectIDs.send(snapshot.itemIdentifiers)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import MastodonSDK
|
||||||
extension APIService {
|
extension APIService {
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private static let clientName = "Skimming"
|
private static let clientName = "Mastodon for iOS (Development)"
|
||||||
#else
|
#else
|
||||||
private static let clientName = "Mastodon for iOS"
|
private static let clientName = "Mastodon for iOS"
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -16,7 +16,7 @@ extension APIService {
|
||||||
|
|
||||||
public func followRequest(
|
public func followRequest(
|
||||||
userID: Mastodon.Entity.Account.ID,
|
userID: Mastodon.Entity.Account.ID,
|
||||||
query: Mastodon.API.Account.FollowReqeustQuery,
|
query: Mastodon.API.Account.FollowRequestQuery,
|
||||||
authenticationBox: MastodonAuthenticationBox
|
authenticationBox: MastodonAuthenticationBox
|
||||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
|
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
|
||||||
let response = try await Mastodon.API.Account.followRequest(
|
let response = try await Mastodon.API.Account.followRequest(
|
||||||
|
@ -51,4 +51,17 @@ extension APIService {
|
||||||
return response
|
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(
|
let blockedIds = try await apiService.getBlocked(
|
||||||
authenticationBox: authBox
|
authenticationBox: authBox
|
||||||
).value.map { $0.id }
|
).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.followingUserIds = followingIds
|
||||||
authBox.inMemoryCache.blockedUserIds = blockedIds
|
authBox.inMemoryCache.blockedUserIds = blockedIds
|
||||||
}
|
}
|
||||||
|
|
|
@ -1484,10 +1484,10 @@ public enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public enum SuggestionAccount {
|
public enum SuggestionAccount {
|
||||||
/// When you follow someone, you’ll see their posts in your home feed.
|
/// Follow All
|
||||||
public static let followExplain = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowExplain", fallback: "When you follow someone, you’ll see their posts in your home feed.")
|
public static let followAll = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowAll", fallback: "Follow All")
|
||||||
/// Find People to Follow
|
/// Popular on Mastodon
|
||||||
public static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title", fallback: "Find People to Follow")
|
public static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title", fallback: "Popular on Mastodon")
|
||||||
}
|
}
|
||||||
public enum Thread {
|
public enum Thread {
|
||||||
/// Post
|
/// Post
|
||||||
|
|
|
@ -517,8 +517,8 @@ uploaded to Mastodon.";
|
||||||
"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out";
|
"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out";
|
||||||
"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone";
|
"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone";
|
||||||
"Scene.Settings.Title" = "Settings";
|
"Scene.Settings.Title" = "Settings";
|
||||||
"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed.";
|
"Scene.SuggestionAccount.Title" = "Popular on Mastodon";
|
||||||
"Scene.SuggestionAccount.Title" = "Find People to Follow";
|
"Scene.SuggestionAccount.FollowAll" = "Follow All";
|
||||||
"Scene.Thread.BackTitle" = "Post";
|
"Scene.Thread.BackTitle" = "Post";
|
||||||
"Scene.Thread.Title" = "Post from %@";
|
"Scene.Thread.Title" = "Post from %@";
|
||||||
"Scene.Welcome.Education.A11Y.WhatIsMastodon.Title" = "What is Mastodon?";
|
"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.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
"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.Signout" = "Sign Out";
|
||||||
"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone";
|
"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone";
|
||||||
"Scene.Settings.Title" = "Settings";
|
"Scene.Settings.Title" = "Settings";
|
||||||
"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed.";
|
"Scene.SuggestionAccount.Title" = "Popular on Mastodon";
|
||||||
"Scene.SuggestionAccount.Title" = "Find People to Follow";
|
"Scene.SuggestionAccount.FollowAll" = "Follow All";
|
||||||
"Scene.Thread.BackTitle" = "Post";
|
"Scene.Thread.BackTitle" = "Post";
|
||||||
"Scene.Thread.Title" = "Post from %@";
|
"Scene.Thread.Title" = "Post from %@";
|
||||||
"Scene.Welcome.Education.A11Y.WhatIsMastodon.Title" = "What is Mastodon?";
|
"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.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
"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
|
// MARK: - Account credentials
|
||||||
extension Mastodon.API.Account {
|
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 {
|
static func acceptFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL {
|
||||||
return Mastodon.API.endpointURL(domain: domain)
|
return Mastodon.API.endpointURL(domain: domain)
|
||||||
|
@ -25,7 +30,7 @@ extension Mastodon.API.Account {
|
||||||
.appendingPathComponent("reject")
|
.appendingPathComponent("reject")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accept Follow
|
/// Pending Follow Requests
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// - Since: 0.0.0
|
/// - Since: 0.0.0
|
||||||
|
@ -37,6 +42,38 @@ extension Mastodon.API.Account {
|
||||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
/// - userID: ID of the account in the database
|
/// - userID: ID of the account in the database
|
||||||
/// - authorization: User token
|
/// - 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
|
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||||
public static func acceptFollowRequest(
|
public static func acceptFollowRequest(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
|
@ -63,7 +100,7 @@ extension Mastodon.API.Account {
|
||||||
/// - Since: 0.0.0
|
/// - Since: 0.0.0
|
||||||
/// - Version: 3.3.0
|
/// - Version: 3.3.0
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/)
|
/// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/#reject)
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - session: `URLSession`
|
/// - session: `URLSession`
|
||||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
@ -92,7 +129,7 @@ extension Mastodon.API.Account {
|
||||||
|
|
||||||
extension Mastodon.API.Account {
|
extension Mastodon.API.Account {
|
||||||
|
|
||||||
public enum FollowReqeustQuery {
|
public enum FollowRequestQuery {
|
||||||
case accept
|
case accept
|
||||||
case reject
|
case reject
|
||||||
}
|
}
|
||||||
|
@ -101,7 +138,7 @@ extension Mastodon.API.Account {
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
userID: Mastodon.Entity.Account.ID,
|
userID: Mastodon.Entity.Account.ID,
|
||||||
query: FollowReqeustQuery,
|
query: FollowRequestQuery,
|
||||||
authorization: Mastodon.API.OAuth.Authorization
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||||
switch query {
|
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 final class UserView: UIView {
|
||||||
|
|
||||||
public enum ButtonState {
|
public enum ButtonState {
|
||||||
case none, loading, follow, unfollow, blocked
|
case none, loading, follow, request, pending, unfollow, blocked
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentButtonState: ButtonState = .none
|
private var currentButtonState: ButtonState = .none
|
||||||
|
@ -99,7 +99,8 @@ public final class UserView: UIView {
|
||||||
label.textColor = .secondaryLabel
|
label.textColor = .secondaryLabel
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private let followButtonWrapper = UIView()
|
||||||
private let followButton: FollowButton = {
|
private let followButton: FollowButton = {
|
||||||
let button = FollowButton()
|
let button = FollowButton()
|
||||||
button.cornerRadius = 10
|
button.cornerRadius = 10
|
||||||
|
@ -149,10 +150,7 @@ extension UserView {
|
||||||
|
|
||||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
containerStackView.addArrangedSubview(avatarButton)
|
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: .vertical)
|
||||||
avatarButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
avatarButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
@ -162,7 +160,19 @@ extension UserView {
|
||||||
containerStackView.addArrangedSubview(labelStackView)
|
containerStackView.addArrangedSubview(labelStackView)
|
||||||
|
|
||||||
// follow button
|
// 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()
|
let nameStackView = UIStackView()
|
||||||
nameStackView.axis = .horizontal
|
nameStackView.axis = .horizontal
|
||||||
|
@ -181,7 +191,14 @@ extension UserView {
|
||||||
authorUsernameLabel.setContentCompressionResistancePriority(.defaultHigh - 1, for: .horizontal)
|
authorUsernameLabel.setContentCompressionResistancePriority(.defaultHigh - 1, for: .horizontal)
|
||||||
|
|
||||||
labelStackView.addArrangedSubview(nameStackView)
|
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 verifiedSpacerView = UIView()
|
||||||
let verifiedStackTrailingSpacerView = 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 {
|
public extension UserView {
|
||||||
private func prepareButtonStateLayout(for state: ButtonState) {
|
private func prepareButtonStateLayout(for state: ButtonState) {
|
||||||
switch state {
|
switch state {
|
||||||
|
@ -269,6 +259,7 @@ public extension UserView {
|
||||||
prepareButtonStateLayout(for: state)
|
prepareButtonStateLayout(for: state)
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
|
|
||||||
case .loading:
|
case .loading:
|
||||||
followButton.isHidden = false
|
followButton.isHidden = false
|
||||||
followButton.setTitle(nil, for: .normal)
|
followButton.setTitle(nil, for: .normal)
|
||||||
|
@ -279,7 +270,19 @@ public extension UserView {
|
||||||
followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal)
|
followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal)
|
||||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
||||||
followButton.setTitleColor(.white, 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:
|
case .unfollow:
|
||||||
followButton.isHidden = false
|
followButton.isHidden = false
|
||||||
followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)
|
followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)
|
||||||
|
|
Loading…
Reference in New Issue