mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
Make Profile-screen use Mastodon.Entity.Account instead of MastodonUser (#1192)
- Make Profile-screen use Mastodon.Entity.Account instead of MastodonUser - Fix things left and right - Just like with the Status-refactoring I'm expecting more bugs to show up over time.
This commit is contained in:
commit
db12ea4aa9
@ -62,7 +62,6 @@
|
||||
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; };
|
||||
2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
|
||||
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; };
|
||||
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */; };
|
||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||
@ -135,6 +134,7 @@
|
||||
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; };
|
||||
D80F627C2B5C32C500877059 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80F627A2B5C32C500877059 /* NotificationView.swift */; };
|
||||
D81439862AD415DE0071A88F /* AboutInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81439852AD415DE0071A88F /* AboutInstance.swift */; };
|
||||
D81439882AD450A40071A88F /* AboutInstanceTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81439872AD450A40071A88F /* AboutInstanceTableViewDataSource.swift */; };
|
||||
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */; };
|
||||
@ -164,14 +164,12 @@
|
||||
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
|
||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */; };
|
||||
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98782B0F622B0045EC2B /* SearchHistory.swift */; };
|
||||
D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; };
|
||||
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; };
|
||||
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; };
|
||||
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.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 */; };
|
||||
D8CF45832B50893900C84D70 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CF45822B50893900C84D70 /* Tab.swift */; };
|
||||
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; };
|
||||
D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; };
|
||||
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
|
||||
@ -410,7 +408,6 @@
|
||||
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */; };
|
||||
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */; };
|
||||
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; };
|
||||
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; };
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
||||
@ -429,7 +426,6 @@
|
||||
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */; };
|
||||
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; };
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
||||
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
||||
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */; };
|
||||
@ -443,7 +439,6 @@
|
||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */; };
|
||||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; };
|
||||
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; };
|
||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
|
||||
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
|
||||
DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; };
|
||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
|
||||
@ -700,7 +695,6 @@
|
||||
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = "<group>"; };
|
||||
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = "<group>"; };
|
||||
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Timeline.swift"; sourceTree = "<group>"; };
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||
@ -794,6 +788,7 @@
|
||||
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = "<group>"; };
|
||||
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
|
||||
D80F627A2B5C32C500877059 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
|
||||
D81439852AD415DE0071A88F /* AboutInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstance.swift; sourceTree = "<group>"; };
|
||||
D81439872AD450A40071A88F /* AboutInstanceTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstanceTableViewDataSource.swift; sourceTree = "<group>"; };
|
||||
D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = "<group>"; };
|
||||
@ -839,14 +834,12 @@
|
||||
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SearchHistory.swift"; sourceTree = "<group>"; };
|
||||
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||
D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = "<group>"; };
|
||||
D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
|
||||
D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModel.swift; 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>"; };
|
||||
D8CF45822B50893900C84D70 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = "<group>"; };
|
||||
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = "<group>"; };
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||
@ -1140,7 +1133,6 @@
|
||||
DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCommentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewController.swift; sourceTree = "<group>"; };
|
||||
DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = "<group>"; };
|
||||
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||
@ -1171,7 +1163,6 @@
|
||||
DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = "<group>"; };
|
||||
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
|
||||
DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewController.swift; sourceTree = "<group>"; };
|
||||
DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = "<group>"; };
|
||||
@ -1184,7 +1175,6 @@
|
||||
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
||||
DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
|
||||
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||
@ -1555,7 +1545,6 @@
|
||||
children = (
|
||||
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */,
|
||||
DB0FCB992797F7AD006C02E2 /* UserView+Configuration.swift */,
|
||||
DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */,
|
||||
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
||||
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
|
||||
DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */,
|
||||
@ -1724,7 +1713,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */,
|
||||
D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */,
|
||||
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */,
|
||||
);
|
||||
path = "TableView-Components";
|
||||
@ -1833,6 +1821,15 @@
|
||||
path = Privacy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D80F627E2B5C32E400877059 /* NotificationView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D80F627A2B5C32C500877059 /* NotificationView.swift */,
|
||||
DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */,
|
||||
);
|
||||
path = NotificationView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D81A22732AB4641F00905D71 /* Search Results Overview */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1898,24 +1895,6 @@
|
||||
path = Localization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8AC98742B0F615E0045EC2B /* Persistence */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98772B0F62230045EC2B /* Model */,
|
||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */,
|
||||
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8AC98772B0F62230045EC2B /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8E5C347296DB896007E76A7 /* Edit History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2201,7 +2180,6 @@
|
||||
DB427DD425BAA00100D1B89D /* Mastodon */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98742B0F615E0045EC2B /* Persistence */,
|
||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||
2D76319C25C151DE00929FB9 /* Diffable */,
|
||||
@ -2594,6 +2572,7 @@
|
||||
DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */,
|
||||
DB852D1A26FAED0100FC9D81 /* Sidebar */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
D8CF45822B50893900C84D70 /* Tab.swift */,
|
||||
);
|
||||
path = Root;
|
||||
sourceTree = "<group>";
|
||||
@ -2707,7 +2686,6 @@
|
||||
DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */,
|
||||
DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */,
|
||||
DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */,
|
||||
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
@ -2766,6 +2744,7 @@
|
||||
children = (
|
||||
DB63F765279A5E5600455B82 /* NotificationTimeline */,
|
||||
2D35237F26256F470031AF25 /* Cell */,
|
||||
D80F627E2B5C32E400877059 /* NotificationView */,
|
||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
|
||||
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
|
||||
);
|
||||
@ -2788,8 +2767,6 @@
|
||||
DBFEEC97279BDC6A004F81DD /* About */,
|
||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
||||
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */,
|
||||
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
@ -3755,7 +3732,6 @@
|
||||
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
|
||||
D81A94172B07A1D30067A19D /* ProfileCardView+Configuration.swift in Sources */,
|
||||
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */,
|
||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||
D82BD7552ABC73AF009A374A /* NotificationPolicyTableViewCell.swift in Sources */,
|
||||
DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */,
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
|
||||
@ -3821,6 +3797,7 @@
|
||||
DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */,
|
||||
D80F627C2B5C32C500877059 /* NotificationView.swift in Sources */,
|
||||
DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */,
|
||||
DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */,
|
||||
DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */,
|
||||
@ -3857,7 +3834,6 @@
|
||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||
DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */,
|
||||
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */,
|
||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||
DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */,
|
||||
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
|
||||
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,
|
||||
@ -3871,7 +3847,6 @@
|
||||
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */,
|
||||
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
|
||||
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
|
||||
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */,
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
@ -3879,7 +3854,6 @@
|
||||
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
||||
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */,
|
||||
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */,
|
||||
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */,
|
||||
DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */,
|
||||
@ -3918,7 +3892,6 @@
|
||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
||||
DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */,
|
||||
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
|
||||
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */,
|
||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
||||
@ -3948,7 +3921,6 @@
|
||||
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
|
||||
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
|
||||
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
|
||||
D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */,
|
||||
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */,
|
||||
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */,
|
||||
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */,
|
||||
@ -3984,7 +3956,6 @@
|
||||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
||||
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */,
|
||||
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
||||
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */,
|
||||
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
|
||||
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
|
||||
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */,
|
||||
@ -4017,6 +3988,7 @@
|
||||
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */,
|
||||
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */,
|
||||
DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */,
|
||||
D8CF45832B50893900C84D70 /* Tab.swift in Sources */,
|
||||
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */,
|
||||
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */,
|
||||
DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */,
|
||||
|
@ -46,12 +46,13 @@ final public class SceneCoordinator {
|
||||
self.appContext = appContext
|
||||
|
||||
scene.session.sceneCoordinator = self
|
||||
|
||||
|
||||
appContext.notificationService.requestRevealNotificationPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] pushNotification in
|
||||
guard let self = self else { return }
|
||||
Task {
|
||||
.sink(receiveValue: {
|
||||
[weak self] pushNotification in
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
|
||||
let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
|
||||
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
|
||||
@ -67,54 +68,76 @@ final public class SceneCoordinator {
|
||||
let userID = authentication.userID
|
||||
let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
|
||||
guard isSuccess else { return }
|
||||
|
||||
|
||||
self.setup()
|
||||
try await Task.sleep(nanoseconds: .second * 1)
|
||||
|
||||
|
||||
// redirect to notifications tab
|
||||
self.switchToTabBar(tab: .notifications)
|
||||
|
||||
// Delay in next run loop
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Note:
|
||||
// show (push) on phone and pad
|
||||
let from: UIViewController? = {
|
||||
if let splitViewController = self.splitViewController {
|
||||
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
|
||||
// compact
|
||||
return splitViewController.compactMainTabBarViewController.topMost
|
||||
} else {
|
||||
// expand
|
||||
return splitViewController.contentSplitViewController.mainTabBarController.topMost
|
||||
}
|
||||
|
||||
// Note:
|
||||
// show (push) on phone and pad
|
||||
let from: UIViewController? = {
|
||||
if let splitViewController = self.splitViewController {
|
||||
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
|
||||
// compact
|
||||
return splitViewController.compactMainTabBarViewController.topMost
|
||||
} else {
|
||||
return self.tabBarController.topMost
|
||||
// expand
|
||||
return splitViewController.contentSplitViewController.mainTabBarController.topMost
|
||||
}
|
||||
}()
|
||||
|
||||
// show notification related content
|
||||
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
|
||||
guard let authContext = self.authContext else { return }
|
||||
let notificationID = String(pushNotification.notificationID)
|
||||
|
||||
switch type {
|
||||
case .follow:
|
||||
let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
|
||||
_ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
|
||||
case .followRequest:
|
||||
// do nothing
|
||||
break
|
||||
case .mention, .reblog, .favourite, .poll, .status:
|
||||
let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
|
||||
_ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
|
||||
case ._other:
|
||||
assertionFailure()
|
||||
break
|
||||
} else {
|
||||
return self.tabBarController.topMost
|
||||
}
|
||||
} // end DispatchQueue.main.async
|
||||
|
||||
}()
|
||||
|
||||
// show notification related content
|
||||
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
|
||||
guard let authContext = self.authContext else { return }
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return }
|
||||
let notificationID = String(pushNotification.notificationID)
|
||||
|
||||
switch type {
|
||||
case .follow:
|
||||
let account = try await appContext.apiService.notification(
|
||||
notificationID: notificationID,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
).value.account
|
||||
|
||||
let relationship = try await appContext.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: appContext,
|
||||
authContext: authContext,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
_ = self.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: from,
|
||||
transition: .show
|
||||
)
|
||||
case .followRequest:
|
||||
// do nothing
|
||||
break
|
||||
case .mention, .reblog, .favourite, .poll, .status:
|
||||
let threadViewModel = RemoteThreadViewModel(
|
||||
context: appContext,
|
||||
authContext: authContext,
|
||||
notificationID: notificationID
|
||||
)
|
||||
_ = self.present(
|
||||
scene: .thread(viewModel: threadViewModel),
|
||||
from: from,
|
||||
transition: .show
|
||||
)
|
||||
|
||||
case ._other:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return
|
||||
@ -140,7 +163,7 @@ extension SceneCoordinator {
|
||||
case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil)
|
||||
case none
|
||||
}
|
||||
|
||||
|
||||
enum Scene {
|
||||
// onboarding
|
||||
case welcome
|
||||
@ -357,7 +380,7 @@ extension SceneCoordinator {
|
||||
return viewController
|
||||
}
|
||||
|
||||
func switchToTabBar(tab: MainTabBarController.Tab) {
|
||||
func switchToTabBar(tab: Tab) {
|
||||
splitViewController?.contentSplitViewController.currentSupplementaryTab = tab
|
||||
|
||||
splitViewController?.compactMainTabBarViewController.selectedIndex = tab.rawValue
|
||||
@ -472,9 +495,7 @@ private extension SceneCoordinator {
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
viewController = ReportViewController(viewModel: viewModel)
|
||||
case .reportServerRules(let viewModel):
|
||||
let _viewController = ReportServerRulesViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
@ -73,9 +73,6 @@ extension NotificationSection {
|
||||
viewModel: NotificationTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
cell.notificationView.viewModel.context = context
|
||||
cell.notificationView.viewModel.authContext = configuration.authContext
|
||||
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
@ -91,7 +88,8 @@ extension NotificationSection {
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.notificationTableViewCellDelegate
|
||||
delegate: configuration.notificationTableViewCellDelegate,
|
||||
authenticationBox: configuration.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
cell.notificationView.statusView.viewModel.filterContext = configuration.filterContext
|
||||
|
@ -13,7 +13,6 @@ enum ReportItem: Hashable {
|
||||
case header(context: HeaderContext)
|
||||
case status(record: MastodonStatus)
|
||||
case comment(context: CommentContext)
|
||||
case result(record: ManagedObjectRecord<MastodonUser>)
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,6 @@ extension ReportSection {
|
||||
tableView.register(ReportHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: ReportHeadlineTableViewCell.self))
|
||||
tableView.register(ReportStatusTableViewCell.self, forCellReuseIdentifier: String(describing: ReportStatusTableViewCell.self))
|
||||
tableView.register(ReportCommentTableViewCell.self, forCellReuseIdentifier: String(describing: ReportCommentTableViewCell.self))
|
||||
tableView.register(ReportResultActionTableViewCell.self, forCellReuseIdentifier: String(describing: ReportResultActionTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
@ -72,13 +71,6 @@ extension ReportSection {
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .result(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL()))
|
||||
}
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
|
@ -37,7 +37,7 @@ extension UserSection {
|
||||
case .account(let account, let relationship):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell }
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return cell }
|
||||
|
||||
cell.userView.setButtonState(.loading)
|
||||
cell.configure(
|
||||
|
@ -5,8 +5,6 @@
|
||||
// Created by Marcus Kida on 17.11.22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension Persistence.SearchHistory {
|
||||
struct Item: Codable, Hashable, Equatable {
|
||||
let updatedAt: Date
|
||||
let userID: Mastodon.Entity.Account.ID
|
||||
|
||||
let account: Mastodon.Entity.Account?
|
||||
let hashtag: Mastodon.Entity.Tag?
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(userID)
|
||||
hasher.combine(account)
|
||||
hasher.combine(hashtag)
|
||||
}
|
||||
|
||||
public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool {
|
||||
return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID
|
||||
}
|
||||
}
|
||||
}
|
@ -13,56 +13,40 @@ import MastodonSDK
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserBlockAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let apiService = dependency.context.apiService
|
||||
let authBox = dependency.authContext.mastodonAuthenticationBox
|
||||
|
||||
_ = try await apiService.toggleBlock(
|
||||
user: user,
|
||||
authenticationBox: authBox
|
||||
)
|
||||
|
||||
try await dependency.context.apiService.getBlocked(
|
||||
authenticationBox: authBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
}
|
||||
|
||||
static func responseToUserBlockAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account
|
||||
) async throws {
|
||||
account: Mastodon.Entity.Account
|
||||
) async throws -> Mastodon.Entity.Relationship {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let apiService = dependency.context.apiService
|
||||
let authBox = dependency.authContext.mastodonAuthenticationBox
|
||||
|
||||
_ = try await apiService.toggleBlock(
|
||||
user: user,
|
||||
let response = try await apiService.toggleBlock(
|
||||
account: account,
|
||||
authenticationBox: authBox
|
||||
)
|
||||
|
||||
try await dependency.context.apiService.getBlocked(
|
||||
authenticationBox: authBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
let userInfo = [
|
||||
UserInfoKey.relationship: response.value,
|
||||
]
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo)
|
||||
|
||||
return response.value
|
||||
}
|
||||
|
||||
static func responseToDomainBlockAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
account: Mastodon.Entity.Account
|
||||
) async throws -> Mastodon.Entity.Empty {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let apiService = dependency.context.apiService
|
||||
let authBox = dependency.authContext.mastodonAuthenticationBox
|
||||
|
||||
_ = try await apiService.toggleDomainBlock(user: user, authenticationBox: authBox)
|
||||
let response = try await apiService.toggleDomainBlock(account: account, authenticationBox: authBox)
|
||||
|
||||
return response.value
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import class CoreDataStack.Notification
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
@ -15,32 +14,22 @@ import MastodonLocalization
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserFollowAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await dependency.context.apiService.toggleFollow(
|
||||
user: user,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
}
|
||||
|
||||
static func responseToUserFollowAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account
|
||||
account: Mastodon.Entity.Account
|
||||
) async throws -> Mastodon.Entity.Relationship {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let response = try await dependency.context.apiService.toggleFollow(
|
||||
user: user,
|
||||
account: account,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [
|
||||
UserInfoKey.relationship: response
|
||||
])
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@ -50,44 +39,51 @@ extension DataSourceFacade {
|
||||
static func responseToUserFollowRequestAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
notification: MastodonNotification,
|
||||
notificationView: NotificationView,
|
||||
query: Mastodon.API.Account.FollowRequestQuery
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
|
||||
return notification.account.id
|
||||
}
|
||||
|
||||
guard let userID = _userID else {
|
||||
assertionFailure()
|
||||
throw APIService.APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
|
||||
let userID = notification.account.id
|
||||
let state: MastodonFollowRequestState = notification.followRequestState
|
||||
|
||||
guard state.state == .none else {
|
||||
return
|
||||
}
|
||||
|
||||
guard state.state == .none else { return }
|
||||
|
||||
switch query {
|
||||
case .accept:
|
||||
notification.transientFollowRequestState = .init(state: .isAccepting)
|
||||
case .reject:
|
||||
notification.transientFollowRequestState = .init(state: .isRejecting)
|
||||
}
|
||||
|
||||
|
||||
await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox)
|
||||
|
||||
do {
|
||||
_ = try await dependency.context.apiService.followRequest(
|
||||
let newRelationship = try await dependency.context.apiService.followRequest(
|
||||
userID: userID,
|
||||
query: query,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
).value
|
||||
|
||||
switch query {
|
||||
case .accept:
|
||||
notification.transientFollowRequestState = .init(state: .isAccept)
|
||||
notification.followRequestState = .init(state: .isAccept)
|
||||
case .reject:
|
||||
break
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [
|
||||
UserInfoKey.relationship: newRelationship
|
||||
])
|
||||
|
||||
await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox)
|
||||
} catch {
|
||||
// reset state when failure
|
||||
notification.transientFollowRequestState = .init(state: .none)
|
||||
|
||||
await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox)
|
||||
|
||||
if let error = error as? Mastodon.API.Error {
|
||||
switch error.httpResponseStatus {
|
||||
case .notFound:
|
||||
@ -103,36 +99,25 @@ extension DataSourceFacade {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch query {
|
||||
case .accept:
|
||||
notification.transientFollowRequestState = .init(state: .isAccept)
|
||||
notification.followRequestState = .init(state: .isAccept)
|
||||
case .reject:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToShowHideReblogAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
_ = try await dependency.context.apiService.toggleShowReblogs(
|
||||
for: user,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox)
|
||||
}
|
||||
|
||||
static func responseToShowHideReblogAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
account: Mastodon.Entity.Account
|
||||
) async throws {
|
||||
_ = try await dependency.context.apiService.toggleShowReblogs(
|
||||
for: user,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox)
|
||||
let newRelationship = try await dependency.context.apiService.toggleShowReblogs(
|
||||
for: account,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
let userInfo = [
|
||||
UserInfoKey.relationship: newRelationship,
|
||||
]
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
@ -28,30 +28,4 @@ extension DataSourceFacade {
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func coordinateToHashtagScene(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
tag: ManagedObjectRecord<Tag>
|
||||
) async {
|
||||
let managedObjectContext = provider.context.managedObjectContext
|
||||
let _name: String? = try? await managedObjectContext.perform {
|
||||
guard let tag = tag.object(in: managedObjectContext) else { return nil }
|
||||
return tag.name
|
||||
}
|
||||
|
||||
guard let name = _name else { return }
|
||||
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
hashtag: name
|
||||
)
|
||||
|
||||
_ = provider.coordinator.present(
|
||||
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
|
||||
from: provider,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,6 @@ extension DataSourceFacade {
|
||||
status: MastodonStatus,
|
||||
previewContext: AttachmentPreviewContext
|
||||
) async throws {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let status = status.reblog ?? status
|
||||
let attachments = status.entity.mastodonAttachments
|
||||
|
||||
@ -140,87 +139,61 @@ extension DataSourceFacade {
|
||||
case profileBanner(ProfileHeaderView)
|
||||
}
|
||||
|
||||
func thumbnail() async -> UIImage? {
|
||||
return await imageView.image
|
||||
func thumbnail() -> UIImage? {
|
||||
return imageView.image
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func coordinateToMediaPreviewScene(
|
||||
dependency: NeedsDependency & MediaPreviewableViewController,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
account: Mastodon.Entity.Account,
|
||||
previewContext: ImagePreviewContext
|
||||
) async throws {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
|
||||
let avatarAssetURL = account.avatar
|
||||
let headerAssetURL = account.header
|
||||
|
||||
let thumbnail = previewContext.thumbnail()
|
||||
|
||||
var _avatarAssetURL: String?
|
||||
var _headerAssetURL: String?
|
||||
|
||||
try await managedObjectContext.perform {
|
||||
guard let user = user.object(in: managedObjectContext) else { return }
|
||||
_avatarAssetURL = user.avatar
|
||||
_headerAssetURL = user.header
|
||||
let source: MediaPreviewTransitionItem.Source
|
||||
switch previewContext.containerView {
|
||||
case .profileAvatar(let view): source = .profileAvatar(view)
|
||||
case .profileBanner(let view): source = .profileBanner(view)
|
||||
}
|
||||
|
||||
let thumbnail = await previewContext.thumbnail()
|
||||
|
||||
let source: MediaPreviewTransitionItem.Source = {
|
||||
|
||||
let mediaPreviewTransitionItem = MediaPreviewTransitionItem(
|
||||
source: source,
|
||||
previewableViewController: dependency
|
||||
)
|
||||
|
||||
let imageView = previewContext.imageView
|
||||
mediaPreviewTransitionItem.initialFrame = imageView.superview?.convert(imageView.frame, to: nil)
|
||||
mediaPreviewTransitionItem.image = thumbnail
|
||||
mediaPreviewTransitionItem.aspectRatio = thumbnail?.size ?? CGSize(width: 100, height: 100)
|
||||
mediaPreviewTransitionItem.sourceImageViewCornerRadius = {
|
||||
switch previewContext.containerView {
|
||||
case .profileAvatar(let view): return .profileAvatar(view)
|
||||
case .profileBanner(let view): return .profileBanner(view)
|
||||
}
|
||||
}()
|
||||
|
||||
let mediaPreviewTransitionItem: MediaPreviewTransitionItem = {
|
||||
let item = MediaPreviewTransitionItem(
|
||||
source: source,
|
||||
previewableViewController: dependency
|
||||
)
|
||||
|
||||
let imageView = previewContext.imageView
|
||||
item.initialFrame = {
|
||||
let initialFrame = imageView.superview!.convert(imageView.frame, to: nil)
|
||||
assert(initialFrame != .zero)
|
||||
return initialFrame
|
||||
}()
|
||||
|
||||
item.image = thumbnail
|
||||
|
||||
item.aspectRatio = {
|
||||
if let thumbnail = thumbnail {
|
||||
return thumbnail.size
|
||||
}
|
||||
return CGSize(width: 100, height: 100)
|
||||
}()
|
||||
|
||||
item.sourceImageViewCornerRadius = {
|
||||
switch previewContext.containerView {
|
||||
case .profileAvatar:
|
||||
return ProfileHeaderView.avatarImageViewCornerRadius
|
||||
case .profileBanner:
|
||||
return 0
|
||||
}
|
||||
}()
|
||||
|
||||
return item
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
let mediaPreviewItem: MediaPreviewViewModel.PreviewItem = {
|
||||
switch previewContext.containerView {
|
||||
|
||||
let mediaPreviewItem: MediaPreviewViewModel.PreviewItem
|
||||
switch previewContext.containerView {
|
||||
case .profileAvatar:
|
||||
return .profileAvatar(.init(
|
||||
assetURL: _avatarAssetURL,
|
||||
mediaPreviewItem = .profileAvatar(.init(
|
||||
assetURL: avatarAssetURL,
|
||||
thumbnail: thumbnail
|
||||
))
|
||||
case .profileBanner:
|
||||
return .profileBanner(.init(
|
||||
assetURL: _headerAssetURL,
|
||||
mediaPreviewItem = .profileBanner(.init(
|
||||
assetURL: headerAssetURL,
|
||||
thumbnail: thumbnail
|
||||
))
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
guard mediaPreviewItem.isAssetURLValid else {
|
||||
return
|
||||
}
|
||||
|
@ -6,20 +6,28 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserMuteAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
account: Mastodon.Entity.Account
|
||||
) async throws -> Mastodon.Entity.Relationship {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await dependency.context.apiService.toggleMute(
|
||||
user: user,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
let response = try await dependency.context.apiService.toggleMute(
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox,
|
||||
account: account
|
||||
)
|
||||
} // end func
|
||||
|
||||
let userInfo = [
|
||||
UserInfoKey.relationship: response.value,
|
||||
]
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo)
|
||||
|
||||
return response.value
|
||||
}
|
||||
}
|
||||
|
@ -41,58 +41,88 @@ extension DataSourceFacade {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
await coordinateToProfileScene(
|
||||
provider: provider,
|
||||
account: redirectRecord
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async {
|
||||
guard let user = user.object(in: provider.context.managedObjectContext) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
optionalMastodonUser: user
|
||||
)
|
||||
|
||||
_ = provider.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: provider,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
username: String,
|
||||
domain: String
|
||||
) async {
|
||||
provider.coordinator.showLoading()
|
||||
|
||||
do {
|
||||
guard let account = try await provider.context.apiService.fetchUser(
|
||||
username: username,
|
||||
domain: domain,
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
||||
) else {
|
||||
return provider.coordinator.hideLoading()
|
||||
}
|
||||
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
await coordinateToProfileScene(provider: provider, account: account)
|
||||
} catch {
|
||||
provider.coordinator.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
domain: String,
|
||||
accountID: String
|
||||
) async {
|
||||
provider.coordinator.showLoading()
|
||||
|
||||
do {
|
||||
let account = try await provider.context.apiService.accountInfo(
|
||||
domain: domain,
|
||||
userID: accountID,
|
||||
authorization: provider.authContext.mastodonAuthenticationBox.userAuthorization
|
||||
).value
|
||||
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
await coordinateToProfileScene(provider: provider, account: account)
|
||||
} catch {
|
||||
provider.coordinator.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public static func coordinateToProfileScene(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
account: Mastodon.Entity.Account
|
||||
) async {
|
||||
provider.coordinator.showLoading()
|
||||
|
||||
guard let domain = account.domain else { return provider.coordinator.hideLoading() }
|
||||
|
||||
Task {
|
||||
do {
|
||||
let user = try await provider.context.apiService.fetchUser(username: account.username,
|
||||
domain: domain,
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox)
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
if let user {
|
||||
await coordinateToProfileScene(provider: provider, user: user.asRecord)
|
||||
}
|
||||
} catch {
|
||||
provider.coordinator.hideLoading()
|
||||
}
|
||||
|
||||
guard let me = provider.authContext.mastodonAuthenticationBox.authentication.account(),
|
||||
let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else {
|
||||
return provider.coordinator.hideLoading()
|
||||
}
|
||||
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
|
||||
_ = provider.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: provider,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,74 +143,31 @@ extension DataSourceFacade {
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let mentions = status.entity.mentions ?? []
|
||||
|
||||
|
||||
guard let mention = mentions.first(where: { $0.url == href }) else {
|
||||
_ = provider.coordinator.present(
|
||||
_ = provider.coordinator.present(
|
||||
scene: .safari(url: url),
|
||||
from: provider,
|
||||
transition: .safariPresent(animated: true, completion: nil)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let userID = mention.id
|
||||
let profileViewModel: ProfileViewModel = {
|
||||
// check if self
|
||||
guard userID != provider.authContext.mastodonAuthenticationBox.userID else {
|
||||
return MeProfileViewModel(context: provider.context, authContext: provider.authContext)
|
||||
}
|
||||
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
|
||||
let _user = provider.context.managedObjectContext.safeFetch(request).first
|
||||
|
||||
if let user = _user {
|
||||
return ProfileViewModel(context: provider.context, authContext: provider.authContext, optionalMastodonUser: user)
|
||||
} else {
|
||||
return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = provider.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: provider,
|
||||
transition: .show
|
||||
)
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: provider, domain: domain, accountID: mention.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
struct ProfileActionMenuContext {
|
||||
let isMuting: Bool
|
||||
let isBlocking: Bool
|
||||
let isMyself: Bool
|
||||
|
||||
let cell: UITableViewCell?
|
||||
let sourceView: UIView?
|
||||
let barButtonItem: UIBarButtonItem?
|
||||
}
|
||||
|
||||
static func createActivityViewController(
|
||||
dependency: NeedsDependency,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws -> UIActivityViewController? {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let activityItems: [Any] = try await managedObjectContext.perform {
|
||||
guard let user = user.object(in: managedObjectContext) else { return [] }
|
||||
return user.activityItems
|
||||
}
|
||||
guard !activityItems.isEmpty else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
|
||||
let activityViewController = await UIActivityViewController(
|
||||
activityItems: activityItems,
|
||||
account: Mastodon.Entity.Account
|
||||
) -> UIActivityViewController {
|
||||
|
||||
let activityViewController = UIActivityViewController(
|
||||
activityItems: [account.url],
|
||||
applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)]
|
||||
)
|
||||
return activityViewController
|
||||
|
@ -27,7 +27,7 @@ extension DataSourceFacade {
|
||||
hashtag: nil
|
||||
)
|
||||
|
||||
try? FileManager.default.addSearchItem(searchEntry)
|
||||
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
|
||||
case .hashtag(let tag):
|
||||
|
||||
let now = Date()
|
||||
@ -39,11 +39,9 @@ extension DataSourceFacade {
|
||||
hashtag: tag
|
||||
)
|
||||
|
||||
try? FileManager.default.addSearchItem(searchEntry)
|
||||
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
|
||||
case .status:
|
||||
break
|
||||
case .user(_):
|
||||
break
|
||||
case .notification:
|
||||
break
|
||||
|
||||
|
@ -137,18 +137,18 @@ extension DataSourceFacade {
|
||||
extension DataSourceFacade {
|
||||
|
||||
struct MenuContext {
|
||||
let author: ManagedObjectRecord<MastodonUser>? // todo: Remove once IOS-192 is ready
|
||||
let authorEntity: Mastodon.Entity.Account?
|
||||
let author: Mastodon.Entity.Account
|
||||
let statusViewModel: StatusView.ViewModel?
|
||||
let button: UIButton?
|
||||
let barButtonItem: UIBarButtonItem?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func responseToMenuAction(
|
||||
static func responseToMenuAction<T>(
|
||||
dependency: UIViewController & NeedsDependency & AuthContextProvider & DataSourceProvider,
|
||||
action: MastodonMenu.Action,
|
||||
menuContext: MenuContext
|
||||
menuContext: MenuContext,
|
||||
completion: ((T) -> Void)? = { (param: Void) in }
|
||||
) async throws {
|
||||
switch action {
|
||||
case .hideReblogs(let actionContext):
|
||||
@ -169,17 +169,9 @@ extension DataSourceFacade {
|
||||
guard let dependency else { return }
|
||||
|
||||
Task {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
|
||||
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
|
||||
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
}
|
||||
|
||||
guard let user = _user else { return }
|
||||
|
||||
try await DataSourceFacade.responseToShowHideReblogAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
account: menuContext.author
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -200,19 +192,17 @@ extension DataSourceFacade {
|
||||
title: actionContext.isMuting ? L10n.Common.Controls.Friendship.unmute : L10n.Common.Controls.Friendship.mute,
|
||||
style: .destructive
|
||||
) { [weak dependency] _ in
|
||||
guard let dependency = dependency else { return }
|
||||
guard let dependency else { return }
|
||||
Task {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
|
||||
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
|
||||
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
}
|
||||
guard let user = _user else { return }
|
||||
try await DataSourceFacade.responseToUserMuteAction(
|
||||
let newRelationship = try await DataSourceFacade.responseToUserMuteAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
account: menuContext.author
|
||||
)
|
||||
} // end Task
|
||||
|
||||
if let completion, let relationship = newRelationship as? T {
|
||||
completion(relationship)
|
||||
}
|
||||
}
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
@ -228,52 +218,44 @@ extension DataSourceFacade {
|
||||
title: actionContext.isBlocking ? L10n.Common.Controls.Friendship.unblock : L10n.Common.Controls.Friendship.block,
|
||||
style: .destructive
|
||||
) { [weak dependency] _ in
|
||||
guard let dependency = dependency else { return }
|
||||
guard let dependency else { return }
|
||||
Task {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
|
||||
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
|
||||
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
}
|
||||
guard let user = _user else { return }
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
let newRelationship = try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
account: menuContext.author
|
||||
)
|
||||
} // end Task
|
||||
|
||||
if let completion, let relationship = newRelationship as? T {
|
||||
completion(relationship)
|
||||
}
|
||||
}
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
dependency.present(alertController, animated: true)
|
||||
case .reportUser:
|
||||
Task {
|
||||
guard let user = menuContext.author else { return }
|
||||
|
||||
let reportViewModel = ReportViewModel(
|
||||
context: dependency.context,
|
||||
authContext: dependency.authContext,
|
||||
user: user,
|
||||
status: menuContext.statusViewModel?.originalStatus
|
||||
)
|
||||
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .report(viewModel: reportViewModel),
|
||||
from: dependency,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
)
|
||||
} // end Task
|
||||
|
||||
case .shareUser:
|
||||
guard let user = menuContext.author else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let _activityViewController = try await DataSourceFacade.createActivityViewController(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
guard let relationship = try? await dependency.context.apiService.relationship(forAccounts: [menuContext.author], authenticationBox: dependency.authContext.mastodonAuthenticationBox).value.first else { return }
|
||||
|
||||
let reportViewModel = ReportViewModel(
|
||||
context: dependency.context,
|
||||
authContext: dependency.authContext,
|
||||
account: menuContext.author,
|
||||
relationship: relationship,
|
||||
status: menuContext.statusViewModel?.originalStatus
|
||||
)
|
||||
guard let activityViewController = _activityViewController else { return }
|
||||
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .report(viewModel: reportViewModel),
|
||||
from: dependency,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
)
|
||||
case .shareUser:
|
||||
let activityViewController = DataSourceFacade.createActivityViewController(
|
||||
dependency: dependency,
|
||||
account: menuContext.author
|
||||
)
|
||||
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
@ -284,7 +266,6 @@ extension DataSourceFacade {
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
case .bookmarkStatus:
|
||||
Task {
|
||||
guard let status = menuContext.statusViewModel?.originalStatus else {
|
||||
assertionFailure()
|
||||
return
|
||||
@ -293,30 +274,26 @@ extension DataSourceFacade {
|
||||
provider: dependency,
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
case .shareStatus:
|
||||
Task {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let activityViewController = try await DataSourceFacade.createActivityViewController(
|
||||
dependency: dependency,
|
||||
status: status
|
||||
)
|
||||
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: menuContext.button,
|
||||
barButtonItem: menuContext.barButtonItem
|
||||
),
|
||||
from: dependency,
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
} // end Task
|
||||
let activityViewController = try await DataSourceFacade.createActivityViewController(
|
||||
dependency: dependency,
|
||||
status: status
|
||||
)
|
||||
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: menuContext.button,
|
||||
barButtonItem: menuContext.barButtonItem
|
||||
),
|
||||
from: dependency,
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
case .deleteStatus:
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.DeletePost.title,
|
||||
@ -373,11 +350,8 @@ extension DataSourceFacade {
|
||||
// do nothing, as the translation is reverted in `StatusTableViewCellDelegate` in `DataSourceProvider+StatusTableViewCellDelegate.swift`.
|
||||
break
|
||||
case .followUser(_):
|
||||
|
||||
guard let author = menuContext.author else { return }
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowAction(dependency: dependency,
|
||||
user: author)
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(dependency: dependency,
|
||||
account: menuContext.author)
|
||||
case .blockDomain(let context):
|
||||
let title: String
|
||||
let message: String
|
||||
@ -400,17 +374,11 @@ extension DataSourceFacade {
|
||||
)
|
||||
|
||||
let confirmAction = UIAlertAction(title: actionTitle, style: .destructive ) { [weak dependency] _ in
|
||||
guard let dependency = dependency else { return }
|
||||
guard let dependency else { return }
|
||||
Task {
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
|
||||
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
|
||||
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
}
|
||||
guard let user = _user else { return }
|
||||
try await DataSourceFacade.responseToDomainBlockAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
account: menuContext.author
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,105 +9,15 @@ import MastodonSDK
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserViewButtonAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
account: Mastodon.Entity.Account,
|
||||
buttonState: UserView.ButtonState
|
||||
) async throws {
|
||||
switch buttonState {
|
||||
case .follow:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(userObject.id)
|
||||
}
|
||||
|
||||
case .request:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(userObject.id)
|
||||
}
|
||||
|
||||
case .unfollow:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == userObject.id })
|
||||
}
|
||||
case .blocked:
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(userObject.id)
|
||||
}
|
||||
|
||||
case .pending:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
if let userObject = user.object(in: dependency.context.managedObjectContext) {
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == userObject.id })
|
||||
}
|
||||
case .none, .loading:
|
||||
break //no-op
|
||||
}
|
||||
}
|
||||
|
||||
static func responseToUserViewButtonAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account,
|
||||
buttonState: UserView.ButtonState
|
||||
) async throws {
|
||||
switch buttonState {
|
||||
case .follow:
|
||||
case .follow, .request, .unfollow, .blocked, .pending:
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
account: account
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id)
|
||||
|
||||
case .request:
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id)
|
||||
case .unfollow:
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == user.id })
|
||||
case .blocked:
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id)
|
||||
|
||||
case .pending:
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == user.id })
|
||||
case .none, .loading:
|
||||
break //no-op
|
||||
}
|
||||
|
@ -30,27 +30,42 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
|
||||
return .init(objectID: notification.account.objectID)
|
||||
}
|
||||
guard let author = _author else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
try await DataSourceFacade.responseToMenuAction(
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: .init(
|
||||
author: author,
|
||||
authorEntity: notification.entity.account,
|
||||
statusViewModel: nil,
|
||||
button: button,
|
||||
barButtonItem: nil
|
||||
|
||||
// we only allow to mute/block and to report users on notification-screen
|
||||
switch action {
|
||||
case .muteUser(_), .blockUser(_):
|
||||
_ = try await DataSourceFacade.responseToMenuAction(
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: .init(
|
||||
author: notification.entity.account,
|
||||
statusViewModel: nil,
|
||||
button: button,
|
||||
barButtonItem: nil
|
||||
),
|
||||
completion: { (newRelationship: Mastodon.Entity.Relationship) in
|
||||
notification.relationship = newRelationship
|
||||
Task { @MainActor in
|
||||
notificationView.configure(notification: notification, authenticationBox: self.authContext.mastodonAuthenticationBox)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} // end Task
|
||||
case .reportUser(_):
|
||||
_ = try await DataSourceFacade.responseToMenuAction(
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: .init(
|
||||
author: notification.entity.account,
|
||||
statusViewModel: nil,
|
||||
button: button,
|
||||
barButtonItem: nil
|
||||
)
|
||||
)
|
||||
case .translateStatus(_), .showOriginal, .shareUser(_), .blockDomain(_), .bookmarkStatus(_), .hideReblogs(_), .shareStatus, .deleteStatus, .editStatus, .followUser(_):
|
||||
// Do Nothing
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,16 +86,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
|
||||
return .init(objectID: notification.account.objectID)
|
||||
}
|
||||
guard let author = _author else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: author
|
||||
account: notification.entity.account
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
@ -105,28 +114,14 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
let originalTransientFollowRequestState = notificationView.viewModel.transientFollowRequestState
|
||||
let originalFollowRequestState = notificationView.viewModel.followRequestState
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isAccepting)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isAccepting)
|
||||
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .accept
|
||||
)
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isAccept)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isAccept)
|
||||
} catch {
|
||||
notificationView.viewModel.transientFollowRequestState = originalTransientFollowRequestState
|
||||
notificationView.viewModel.followRequestState = originalFollowRequestState
|
||||
throw error
|
||||
}
|
||||
} // end Task
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
notificationView: notificationView,
|
||||
query: .accept
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -145,30 +140,15 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
let originalTransientFollowRequestState = notificationView.viewModel.transientFollowRequestState
|
||||
let originalFollowRequestState = notificationView.viewModel.followRequestState
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isRejecting)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isRejecting)
|
||||
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .reject
|
||||
)
|
||||
|
||||
notificationView.viewModel.transientFollowRequestState = .init(state: .isReject)
|
||||
notificationView.viewModel.followRequestState = .init(state: .isReject)
|
||||
} catch {
|
||||
notificationView.viewModel.transientFollowRequestState = originalTransientFollowRequestState
|
||||
notificationView.viewModel.followRequestState = originalFollowRequestState
|
||||
throw error
|
||||
}
|
||||
} // end Task
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
notificationView: notificationView,
|
||||
query: .reject
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Status Content
|
||||
@ -267,7 +247,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
|
||||
return
|
||||
}
|
||||
|
||||
let managedObjectContext = self.context.managedObjectContext
|
||||
let _mediaTransitionContext: NotificationMediaTransitionContext? = {
|
||||
guard let status = record.status?.reblog ?? record.status else { return nil }
|
||||
return NotificationMediaTransitionContext(
|
||||
@ -352,9 +331,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
return
|
||||
}
|
||||
|
||||
guard let account = notification.status?.entity.account else { return }
|
||||
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: notification.account.asRecord
|
||||
account: account
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
@ -532,14 +513,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
case .user(let user):
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: user
|
||||
)
|
||||
case .account(let account, _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .notification:
|
||||
assertionFailure("TODO")
|
||||
default:
|
||||
case .hashtag(_):
|
||||
assertionFailure("TODO")
|
||||
}
|
||||
} // end Task
|
||||
|
@ -35,37 +35,23 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
}
|
||||
|
||||
switch await statusView.viewModel.header {
|
||||
case .none:
|
||||
break
|
||||
case .reply:
|
||||
let _replyToAuthor: ManagedObjectRecord<MastodonUser>? = try? await context.managedObjectContext.perform {
|
||||
guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil }
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: inReplyToAccountID)
|
||||
request.fetchLimit = 1
|
||||
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
|
||||
return .init(objectID: author.objectID)
|
||||
}
|
||||
guard let replyToAuthor = _replyToAuthor else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: replyToAuthor
|
||||
)
|
||||
case .none:
|
||||
break
|
||||
case .reply:
|
||||
guard let replyToAccountID = status.entity.inReplyToAccountID else { return }
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self,
|
||||
domain: domain,
|
||||
accountID: replyToAccountID)
|
||||
|
||||
case .repost:
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
target: .reblog, // keep the wrapper for header author
|
||||
status: status
|
||||
)
|
||||
case .repost:
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
target: .reblog, // keep the wrapper for header author
|
||||
status: status
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - avatar button
|
||||
@ -136,16 +122,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
didTapCardWithURL url: URL
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .status(status) = item else {
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.responseToURLAction(
|
||||
provider: self,
|
||||
url: url
|
||||
@ -160,16 +136,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
didTapURL url: URL
|
||||
) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard case let .status(status) = item else {
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
await DataSourceFacade.responseToURLAction(
|
||||
provider: self,
|
||||
url: url
|
||||
@ -463,21 +429,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let status = _status.reblog ?? _status
|
||||
|
||||
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: self.authContext.mastodonAuthenticationBox.domain, id: status.entity.account.id)
|
||||
request.fetchLimit = 1
|
||||
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
|
||||
return .init(objectID: author.objectID)
|
||||
}
|
||||
guard let author = _author else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
if case .translateStatus = action {
|
||||
DispatchQueue.main.async {
|
||||
if let cell = cell as? StatusTableViewCell {
|
||||
@ -511,8 +465,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: .init(
|
||||
author: author,
|
||||
authorEntity: status.entity.account,
|
||||
author: status.entity.account,
|
||||
statusViewModel: statusViewModel,
|
||||
button: button,
|
||||
barButtonItem: nil
|
||||
@ -709,14 +662,14 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
case .user(let user):
|
||||
case .account(let account, _):
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: user
|
||||
account: account
|
||||
)
|
||||
case .notification:
|
||||
assertionFailure("TODO")
|
||||
default:
|
||||
case .hashtag(_):
|
||||
assertionFailure("TODO")
|
||||
}
|
||||
}
|
||||
|
@ -24,43 +24,34 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
|
||||
switch item {
|
||||
case .account(let account, relationship: _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .status(let status):
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
case .user(let user):
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: user
|
||||
)
|
||||
case .hashtag(let tag):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
tag: tag
|
||||
)
|
||||
case .notification(let notification):
|
||||
let _status: MastodonStatus? = notification.status
|
||||
if let status = _status {
|
||||
case .status(let status):
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
target: .status, // remove reblog wrapper
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
} else {
|
||||
let _author: ManagedObjectRecord<MastodonUser>? = notification.account.asRecord
|
||||
if let author = _author {
|
||||
case .hashtag(let tag):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
tag: tag
|
||||
)
|
||||
case .notification(let notification):
|
||||
let _status: MastodonStatus? = notification.status
|
||||
if let status = _status {
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
} else {
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: author
|
||||
account: notification.entity.account
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end Task
|
||||
} // end func
|
||||
|
||||
} // end Task
|
||||
} // end func
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableViewController {
|
||||
|
@ -9,11 +9,9 @@
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import class CoreDataStack.Notification
|
||||
|
||||
enum DataSourceItem: Hashable {
|
||||
case status(record: MastodonStatus)
|
||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case notification(record: MastodonNotification)
|
||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
|
@ -103,25 +103,25 @@ extension AccountListViewModel {
|
||||
authentication: MastodonAuthentication,
|
||||
activeAuthentication: MastodonAuthentication
|
||||
) {
|
||||
guard let user = authentication.user(in: context) else { return }
|
||||
|
||||
guard let account = authentication.account() else { return }
|
||||
|
||||
// avatar
|
||||
cell.avatarButton.avatarImageView.configure(
|
||||
configuration: .init(url: user.avatarImageURL())
|
||||
configuration: .init(url: account.avatarImageURL())
|
||||
)
|
||||
|
||||
// name
|
||||
do {
|
||||
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis.asDictionary)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
cell.nameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
cell.nameLabel.configure(content: PlaintextMetaContent(string: user.displayNameWithFallback))
|
||||
cell.nameLabel.configure(content: PlaintextMetaContent(string: account.displayNameWithFallback))
|
||||
}
|
||||
|
||||
// username
|
||||
let usernameMetaContent = PlaintextMetaContent(string: "@" + user.acctWithDomain)
|
||||
let usernameMetaContent = PlaintextMetaContent(string: "@" + account.acctWithDomain)
|
||||
cell.usernameLabel.configure(content: usernameMetaContent)
|
||||
|
||||
// badge
|
||||
|
@ -119,7 +119,7 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
|
||||
cell.profileCardView.setButtonState(.loading)
|
||||
|
||||
Task {
|
||||
let newRelationship = try await DataSourceFacade.responseToUserFollowAction(dependency: self, user: account)
|
||||
let newRelationship = try await DataSourceFacade.responseToUserFollowAction(dependency: self, account: account)
|
||||
|
||||
let isMe = (account.id == authContext.mastodonAuthenticationBox.userID)
|
||||
|
||||
|
@ -22,7 +22,7 @@ extension ProfileCardView {
|
||||
viewModel.followersCount = account.followersCount
|
||||
viewModel.authorAvatarImageURL = account.avatarImageURL()
|
||||
|
||||
let emojis = account.emojis?.asDictionary ?? [:]
|
||||
let emojis = account.emojis.asDictionary
|
||||
|
||||
do {
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: emojis)
|
||||
|
@ -57,7 +57,7 @@ extension ProfileCardView.ViewModel {
|
||||
private func bindHeader(view: ProfileCardView) {
|
||||
$authorBannerImageURL
|
||||
.sink { url in
|
||||
guard let url = url, !url.absoluteString.hasSuffix("missing.png") else {
|
||||
guard let url, !url.absoluteString.hasSuffix(Mastodon.Entity.Account.missingImageName) else {
|
||||
view.bannerImageView.image = .placeholder(color: .systemGray3)
|
||||
return
|
||||
}
|
||||
|
@ -316,9 +316,9 @@ extension ProfileCardView {
|
||||
buttonState = .none
|
||||
} else if relationship.following {
|
||||
buttonState = .unfollow
|
||||
} else if relationship.blocking || (relationship.domainBlocking ?? false) {
|
||||
} else if relationship.blocking || relationship.domainBlocking {
|
||||
buttonState = .blocked
|
||||
} else if relationship.requested ?? false {
|
||||
} else if relationship.requested {
|
||||
buttonState = .pending
|
||||
} else {
|
||||
buttonState = .follow
|
||||
|
@ -46,7 +46,6 @@ final class DiscoveryPostsViewModel {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.dataController = StatusDataController()
|
||||
// end init
|
||||
|
||||
Task {
|
||||
await checkServerEndpoint()
|
||||
|
@ -38,20 +38,6 @@ final class HashtagTimelineHeaderView: UIView {
|
||||
postsTodayCount: Int(entity.history?.first?.uses ?? "0") ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
static func from(_ entity: Tag) -> Self {
|
||||
Data(
|
||||
name: entity.name,
|
||||
following: entity.following,
|
||||
postCount: entity.histories.reduce(0) { res, acc in
|
||||
res + (Int(acc.uses) ?? 0)
|
||||
},
|
||||
participantsCount: entity.histories.reduce(0) { res, acc in
|
||||
res + (Int(acc.accounts) ?? 0)
|
||||
},
|
||||
postsTodayCount: Int(entity.histories.first?.uses ?? "0") ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let titleLabel = UILabel()
|
||||
|
@ -61,18 +61,7 @@ final class HashtagTimelineViewModel {
|
||||
}
|
||||
|
||||
func viewWillAppear() {
|
||||
let predicate = Tag.predicate(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
name: hashtag
|
||||
)
|
||||
|
||||
guard
|
||||
let object = Tag.findOrFetch(in: context.managedObjectContext, matching: predicate)
|
||||
else {
|
||||
return hashtagDetails.send(hashtagDetails.value?.copy(following: false))
|
||||
}
|
||||
|
||||
hashtagDetails.send(hashtagDetails.value?.copy(following: object.following))
|
||||
hashtagDetails.send(hashtagDetails.value?.copy(following: hashtagEntity.value?.following ?? false))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ extension HomeTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
guard let indexPath = _indexPath else { return nil }
|
||||
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||
guard let item = viewModel?.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ extension HomeTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
viewModel.dataController.update(status: status, intent: intent)
|
||||
viewModel?.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
@ -25,8 +25,8 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: HomeTimelineViewModel!
|
||||
|
||||
var viewModel: HomeTimelineViewModel?
|
||||
|
||||
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||
|
||||
let friendsAssetImageView: UIImageView = {
|
||||
@ -82,7 +82,7 @@ extension HomeTimelineViewController {
|
||||
title = L10n.Scene.HomeTimeline.title
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
|
||||
viewModel.$displaySettingBarButtonItem
|
||||
viewModel?.$displaySettingBarButtonItem
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] displaySettingBarButtonItem in
|
||||
guard let self = self else { return }
|
||||
@ -97,7 +97,7 @@ extension HomeTimelineViewController {
|
||||
navigationItem.titleView = titleView
|
||||
titleView.delegate = self
|
||||
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.state
|
||||
viewModel?.homeTimelineNavigationBarTitleViewModel.state
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] state in
|
||||
@ -106,7 +106,7 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.state
|
||||
viewModel?.homeTimelineNavigationBarTitleViewModel.state
|
||||
.removeDuplicates()
|
||||
.filter { $0 == .publishedButton }
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -137,27 +137,27 @@ extension HomeTimelineViewController {
|
||||
publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
|
||||
viewModel.tableView = tableView
|
||||
viewModel?.tableView = tableView
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
viewModel?.setupDiffableDataSource(
|
||||
tableView: tableView,
|
||||
statusTableViewCellDelegate: self,
|
||||
timelineMiddleLoaderTableViewCellDelegate: self
|
||||
)
|
||||
|
||||
// setup batch fetch
|
||||
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
||||
viewModel.listBatchFetchViewModel.shouldFetch
|
||||
viewModel?.listBatchFetchViewModel.setup(scrollView: tableView)
|
||||
viewModel?.listBatchFetchViewModel.shouldFetch
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard self.view.window != nil else { return }
|
||||
self.viewModel.loadOldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||
self.viewModel?.loadOldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind refresh control
|
||||
viewModel.didLoadLatest
|
||||
viewModel?.didLoadLatest
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
@ -170,8 +170,8 @@ extension HomeTimelineViewController {
|
||||
|
||||
context.publisherService.statusPublishResult.receive(on: DispatchQueue.main).sink { result in
|
||||
if case .success(.edit(let status)) = result {
|
||||
self.viewModel.hasPendingStatusEditReload = true
|
||||
self.viewModel.dataController.update(status: .fromEntity(status.value), intent: .edit)
|
||||
self.viewModel?.hasPendingStatusEditReload = true
|
||||
self.viewModel?.dataController.update(status: .fromEntity(status.value), intent: .edit)
|
||||
}
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
@ -204,24 +204,23 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.timelineIsEmpty
|
||||
viewModel?.timelineIsEmpty
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEmpty in
|
||||
if isEmpty {
|
||||
self?.showEmptyView()
|
||||
|
||||
let userDoesntFollowPeople: Bool
|
||||
if let managedObjectContext = self?.context.managedObjectContext,
|
||||
let authContext = self?.authContext,
|
||||
let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){
|
||||
if let authContext = self?.authContext,
|
||||
let me = authContext.mastodonAuthenticationBox.authentication.account() {
|
||||
userDoesntFollowPeople = me.followersCount == 0
|
||||
} else {
|
||||
userDoesntFollowPeople = true
|
||||
}
|
||||
|
||||
if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople {
|
||||
if (self?.viewModel?.presentedSuggestions == false) && userDoesntFollowPeople {
|
||||
self?.findPeopleButtonPressed(self)
|
||||
self?.viewModel.presentedSuggestions = true
|
||||
self?.viewModel?.presentedSuggestions = true
|
||||
}
|
||||
} else {
|
||||
self?.emptyView.removeFromSuperview()
|
||||
@ -265,16 +264,16 @@ extension HomeTimelineViewController {
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if let timestamp = viewModel.lastAutomaticFetchTimestamp {
|
||||
if let timestamp = viewModel?.lastAutomaticFetchTimestamp {
|
||||
let now = Date()
|
||||
if now.timeIntervalSince(timestamp) > 60 {
|
||||
self.viewModel.lastAutomaticFetchTimestamp = now
|
||||
self.viewModel.homeTimelineNeedRefresh.send()
|
||||
self.viewModel?.lastAutomaticFetchTimestamp = now
|
||||
self.viewModel?.homeTimelineNeedRefresh.send()
|
||||
} else {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
self.viewModel.homeTimelineNeedRefresh.send()
|
||||
self.viewModel?.homeTimelineNeedRefresh.send()
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,7 +284,7 @@ extension HomeTimelineViewController {
|
||||
// do nothing
|
||||
} completion: { _ in
|
||||
// fix AutoLayout cell height not update after rotate issue
|
||||
self.viewModel.cellFrameCache.removeAllObjects()
|
||||
self.viewModel?.cellFrameCache.removeAllObjects()
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
@ -357,7 +356,9 @@ extension HomeTimelineViewController {
|
||||
extension HomeTimelineViewController {
|
||||
|
||||
@objc private func findPeopleButtonPressed(_ sender: Any?) {
|
||||
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext)
|
||||
guard let authContext = viewModel?.authContext else { return }
|
||||
|
||||
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: authContext)
|
||||
suggestionAccountViewModel.delegate = viewModel
|
||||
_ = coordinator.present(
|
||||
scene: .suggestionAccount(viewModel: suggestionAccountViewModel),
|
||||
@ -367,7 +368,9 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
|
||||
@objc private func manuallySearchButtonPressed(_ sender: UIButton) {
|
||||
let searchDetailViewModel = SearchDetailViewModel(authContext: viewModel.authContext)
|
||||
guard let authContext = viewModel?.authContext else { return }
|
||||
|
||||
let searchDetailViewModel = SearchDetailViewModel(authContext: authContext)
|
||||
_ = coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
@ -378,16 +381,18 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
|
||||
@objc private func refreshControlValueChanged(_ sender: RefreshControl) {
|
||||
guard viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.LoadingManually.self) else {
|
||||
guard let viewModel, viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.LoadingManually.self) else {
|
||||
sender.endRefreshing()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@objc func signOutAction(_ sender: UIAction) {
|
||||
guard let authContext = viewModel?.authContext else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||
let userIdentifier = viewModel.authContext.mastodonAuthenticationBox
|
||||
try await context.authenticationService.signOutMastodonUser(authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
let userIdentifier = authContext.mastodonAuthenticationBox
|
||||
FileManager.default.invalidateHomeTimelineCache(for: userIdentifier)
|
||||
FileManager.default.invalidateNotificationsAll(for: userIdentifier)
|
||||
FileManager.default.invalidateNotificationsMentions(for: userIdentifier)
|
||||
@ -401,7 +406,7 @@ extension HomeTimelineViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
switch scrollView {
|
||||
case tableView:
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
viewModel?.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -412,7 +417,7 @@ extension HomeTimelineViewController {
|
||||
case tableView:
|
||||
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else {
|
||||
guard viewModel?.diffableDataSource?.itemIdentifier(for: indexPath) != nil else {
|
||||
return true
|
||||
}
|
||||
// save position
|
||||
@ -429,7 +434,7 @@ extension HomeTimelineViewController {
|
||||
private func savePositionBeforeScrollToTop() {
|
||||
// check save action interval
|
||||
// should not fast than 0.5s to prevent save when scrollToTop on-flying
|
||||
if let record = viewModel.scrollPositionRecord {
|
||||
if let record = viewModel?.scrollPositionRecord {
|
||||
let now = Date()
|
||||
guard now.timeIntervalSince(record.timestamp) > 0.5 else {
|
||||
// skip this save action
|
||||
@ -437,7 +442,7 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
}
|
||||
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let diffableDataSource = viewModel?.diffableDataSource else { return }
|
||||
guard let anchorIndexPaths = tableView.indexPathsForVisibleRows?.sorted() else { return }
|
||||
guard !anchorIndexPaths.isEmpty else { return }
|
||||
let anchorIndexPath = anchorIndexPaths[anchorIndexPaths.count / 2]
|
||||
@ -448,7 +453,7 @@ extension HomeTimelineViewController {
|
||||
let cellFrameInView = tableView.convert(anchorCell.frame, to: view)
|
||||
return cellFrameInView.origin.y
|
||||
}()
|
||||
viewModel.scrollPositionRecord = HomeTimelineViewModel.ScrollPositionRecord(
|
||||
viewModel?.scrollPositionRecord = HomeTimelineViewModel.ScrollPositionRecord(
|
||||
item: anchorItem,
|
||||
offset: offset,
|
||||
timestamp: Date()
|
||||
@ -463,19 +468,19 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
|
||||
private func restorePositionWhenScrollToTop() {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return }
|
||||
guard let record = self.viewModel.scrollPositionRecord,
|
||||
guard let diffableDataSource = viewModel?.diffableDataSource else { return }
|
||||
guard let record = viewModel?.scrollPositionRecord,
|
||||
let indexPath = diffableDataSource.indexPath(for: record.item)
|
||||
else { return }
|
||||
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
viewModel.scrollPositionRecord = nil
|
||||
viewModel?.scrollPositionRecord = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AuthContextProvider
|
||||
extension HomeTimelineViewController: AuthContextProvider {
|
||||
var authContext: AuthContext { viewModel.authContext }
|
||||
var authContext: AuthContext { viewModel!.authContext }
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
@ -508,7 +513,7 @@ extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableView
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
|
||||
viewModel.timelineDidReachEnd()
|
||||
viewModel?.timelineDidReachEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -516,12 +521,12 @@ extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableView
|
||||
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||
extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let diffableDataSource = viewModel?.diffableDataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
Task {
|
||||
await viewModel.loadMore(item: item)
|
||||
await viewModel?.loadMore(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -532,6 +537,8 @@ extension HomeTimelineViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView { return tableView }
|
||||
|
||||
func scrollToTop(animated: Bool) {
|
||||
guard let viewModel else { return }
|
||||
|
||||
if scrollView.contentOffset.y < scrollView.frame.height,
|
||||
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
|
||||
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
|
||||
@ -570,7 +577,7 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) {
|
||||
switch titleView.state {
|
||||
case .newPostButton:
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let diffableDataSource = viewModel?.diffableDataSource else { return }
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return }
|
||||
|
||||
|
@ -92,6 +92,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||
}
|
||||
|
||||
do {
|
||||
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
|
||||
let response = try await viewModel.context.apiService.homeTimeline(
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
@ -99,8 +100,6 @@ extension HomeTimelineViewModel.LoadLatestState {
|
||||
await enter(state: Idle.self)
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.finished)
|
||||
|
||||
viewModel.context.instanceService.updateMutesAndBlocks()
|
||||
|
||||
// stop refresher if no new statuses
|
||||
let statuses = response.value
|
||||
let newStatuses = statuses.filter { status in !latestStatusIDs.contains(where: { $0 == status.reblog?.id || $0 == status.id }) }
|
||||
|
@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension HomeTimelineViewModel {
|
||||
class LoadOldestState: GKState {
|
||||
@ -60,6 +61,8 @@ extension HomeTimelineViewModel.LoadOldestState {
|
||||
}
|
||||
|
||||
do {
|
||||
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
|
||||
|
||||
let response = try await viewModel.context.apiService.homeTimeline(
|
||||
maxID: maxID,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
|
@ -147,7 +147,9 @@ extension HomeTimelineViewModel {
|
||||
// reconfigure item
|
||||
snapshot.reconfigureItems([item])
|
||||
await updateSnapshotUsingReloadData(snapshot: snapshot)
|
||||
|
||||
|
||||
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService)
|
||||
|
||||
// fetch data
|
||||
let maxID = status.id
|
||||
_ = try? await context.apiService.homeTimeline(
|
||||
|
@ -11,6 +11,7 @@ import CoreData
|
||||
import CoreDataStack
|
||||
import Pageboy
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
protocol MediaPreviewPage: UIViewController {
|
||||
func setShowingChrome(_ showingChrome: Bool)
|
||||
@ -151,8 +152,8 @@ extension MediaPreviewViewModel {
|
||||
return true // default valid
|
||||
case .profileBanner(let item):
|
||||
guard let assertURL = item.assetURL else { return false }
|
||||
guard !assertURL.hasSuffix("missing.png") else { return false }
|
||||
return true
|
||||
|
||||
return assertURL.hasSuffix(Mastodon.Entity.Account.missingImageName) == false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension NotificationTableViewCell {
|
||||
final class ViewModel {
|
||||
@ -29,7 +30,8 @@ extension NotificationTableViewCell {
|
||||
func configure(
|
||||
tableView: UITableView,
|
||||
viewModel: ViewModel,
|
||||
delegate: NotificationTableViewCellDelegate?
|
||||
delegate: NotificationTableViewCellDelegate?,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) {
|
||||
if notificationView.frame == .zero {
|
||||
// set status view width
|
||||
@ -41,7 +43,7 @@ extension NotificationTableViewCell {
|
||||
|
||||
switch viewModel.value {
|
||||
case .feed(let feed):
|
||||
notificationView.configure(feed: feed)
|
||||
notificationView.configure(feed: feed, authenticationBox: authenticationBox)
|
||||
}
|
||||
|
||||
self.delegate = delegate
|
||||
@ -57,7 +59,7 @@ extension NotificationTableViewCell {
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
@ -22,10 +22,11 @@ extension NotificationTimelineViewController: DataSourceProvider {
|
||||
|
||||
switch item {
|
||||
case .feed(let feed):
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let item: DataSourceItem? = {
|
||||
guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil }
|
||||
if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) {
|
||||
|
||||
if let notification = feed.notification {
|
||||
let mastodonNotification = MastodonNotification.fromEntity(notification, relationship: nil)
|
||||
return .notification(record: mastodonNotification)
|
||||
} else {
|
||||
return nil
|
||||
@ -36,13 +37,13 @@ extension NotificationTimelineViewController: DataSourceProvider {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) {
|
||||
Task {
|
||||
await viewModel.loadLatest()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
|
@ -38,8 +38,6 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc
|
||||
}()
|
||||
|
||||
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewController {
|
||||
@ -276,7 +274,6 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
|
||||
Task { @MainActor in
|
||||
switch item {
|
||||
@ -295,25 +292,8 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
|
||||
transition: .show
|
||||
)
|
||||
} else {
|
||||
context.managedObjectContext.perform {
|
||||
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
mastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: notification.account.id)
|
||||
mastodonUserRequest.fetchLimit = 1
|
||||
guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else {
|
||||
return
|
||||
}
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: self.context,
|
||||
authContext: self.viewModel.authContext,
|
||||
optionalMastodonUser: mastodonUser
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: notification.account)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
@ -54,7 +54,6 @@ extension NotificationTimelineViewModel.LoadOldestState {
|
||||
let scope = viewModel.scope
|
||||
|
||||
Task {
|
||||
let managedObjectContext = viewModel.context.managedObjectContext
|
||||
let _maxID: Mastodon.Entity.Notification.ID? = lastFeedRecord.notification?.id
|
||||
|
||||
guard let maxID = _maxID else {
|
||||
|
@ -53,18 +53,18 @@ final class NotificationTimelineViewModel {
|
||||
self.authContext = authContext
|
||||
self.scope = scope
|
||||
self.dataController = FeedDataController(context: context, authContext: authContext)
|
||||
|
||||
|
||||
switch scope {
|
||||
case .everything:
|
||||
self.dataController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox))?.map({ notification in
|
||||
MastodonFeed.fromNotification(notification, kind: .notificationAll)
|
||||
MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationAll)
|
||||
}) ?? []
|
||||
case .mentions:
|
||||
self.dataController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in
|
||||
MastodonFeed.fromNotification(notification, kind: .notificationMentions)
|
||||
MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationMentions)
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
|
||||
self.dataController.$records
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -0,0 +1,281 @@
|
||||
//
|
||||
// NotificationView+Configuration.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import Meta
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension NotificationView {
|
||||
public func configure(feed: MastodonFeed, authenticationBox: MastodonAuthenticationBox) {
|
||||
guard let notification = feed.notification else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let entity = MastodonNotification.fromEntity(
|
||||
notification,
|
||||
relationship: feed.relationship
|
||||
)
|
||||
|
||||
configure(notification: entity, authenticationBox: authenticationBox)
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationView {
|
||||
public func configure(notification: MastodonNotification, authenticationBox: MastodonAuthenticationBox) {
|
||||
configureAuthor(notification: notification, authenticationBox: authenticationBox)
|
||||
|
||||
switch notification.entity.type {
|
||||
case .follow:
|
||||
setAuthorContainerBottomPaddingViewDisplay()
|
||||
case .followRequest:
|
||||
setFollowRequestAdaptiveMarginContainerViewDisplay()
|
||||
case .mention, .status:
|
||||
if let status = notification.status {
|
||||
statusView.configure(status: status)
|
||||
setStatusViewDisplay()
|
||||
}
|
||||
case .reblog, .favourite, .poll:
|
||||
if let status = notification.status {
|
||||
quoteStatusView.configure(status: status)
|
||||
setQuoteStatusViewDisplay()
|
||||
}
|
||||
case ._other:
|
||||
setAuthorContainerBottomPaddingViewDisplay()
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func configureAuthor(notification: MastodonNotification, authenticationBox: MastodonAuthenticationBox) {
|
||||
let author = notification.account
|
||||
|
||||
// author avatar
|
||||
let configuration = AvatarImageView.Configuration(url: author.avatarImageURL())
|
||||
avatarButton.avatarImageView.configure(configuration: configuration)
|
||||
avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12)))
|
||||
|
||||
// author name
|
||||
let metaAuthorName: MetaContent
|
||||
do {
|
||||
let content = MastodonContent(content: author.displayNameWithFallback, emojis: author.emojis.asDictionary)
|
||||
metaAuthorName = try MastodonMetaContent.convert(document: content)
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
metaAuthorName = PlaintextMetaContent(string: author.displayNameWithFallback)
|
||||
}
|
||||
authorNameLabel.configure(content: metaAuthorName)
|
||||
|
||||
// username
|
||||
let metaUsername = PlaintextMetaContent(string: "@\(author.acct)")
|
||||
authorUsernameLabel.configure(content: metaUsername)
|
||||
|
||||
let visibility = notification.entity.status?.mastodonVisibility ?? ._other("")
|
||||
visibilityIconImageView.image = visibility.image
|
||||
|
||||
// notification type indicator
|
||||
let notificationIndicatorText: MetaContent?
|
||||
if let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) {
|
||||
// TODO: fix the i18n. The subject should assert place at the string beginning
|
||||
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
|
||||
let content = MastodonContent(content: text, emojis: emojis)
|
||||
guard let metaContent = try? MastodonMetaContent.convert(document: content) else {
|
||||
return PlaintextMetaContent(string: text)
|
||||
}
|
||||
return metaContent
|
||||
}
|
||||
|
||||
switch type {
|
||||
case .follow:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.followedYou,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case .followRequest:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case .mention:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.mentionedYou,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case .reblog:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case .favourite:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case .poll:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.pollHasEnded,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case .status:
|
||||
notificationIndicatorText = createMetaContent(
|
||||
text: .empty,
|
||||
emojis: author.emojis.asDictionary
|
||||
)
|
||||
case ._other:
|
||||
notificationIndicatorText = nil
|
||||
}
|
||||
|
||||
var actions = [UIAccessibilityCustomAction]()
|
||||
|
||||
// these notifications can be directly actioned to view the profile
|
||||
if type != .follow, type != .followRequest {
|
||||
actions.append(
|
||||
UIAccessibilityCustomAction(
|
||||
name: L10n.Common.Controls.Status.showUserProfile,
|
||||
image: nil
|
||||
) { [weak self] _ in
|
||||
guard let self, let delegate = self.delegate else { return false }
|
||||
delegate.notificationView(self, authorAvatarButtonDidPressed: self.avatarButton)
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if type == .followRequest {
|
||||
actions.append(
|
||||
UIAccessibilityCustomAction(
|
||||
name: L10n.Common.Controls.Actions.confirm,
|
||||
image: Asset.Editing.checkmark20.image
|
||||
) { [weak self] _ in
|
||||
guard let self, let delegate = self.delegate else { return false }
|
||||
delegate.notificationView(self, acceptFollowRequestButtonDidPressed: self.acceptFollowRequestButton)
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
actions.append(
|
||||
UIAccessibilityCustomAction(
|
||||
name: L10n.Common.Controls.Actions.delete,
|
||||
image: Asset.Circles.forbidden20.image
|
||||
) { [weak self] _ in
|
||||
guard let self, let delegate = self.delegate else { return false }
|
||||
delegate.notificationView(self, rejectFollowRequestButtonDidPressed: self.rejectFollowRequestButton)
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
notificationActions = actions
|
||||
|
||||
} else {
|
||||
notificationIndicatorText = nil
|
||||
notificationActions = []
|
||||
}
|
||||
|
||||
if let notificationIndicatorText {
|
||||
notificationTypeIndicatorLabel.configure(content: notificationIndicatorText)
|
||||
} else {
|
||||
notificationTypeIndicatorLabel.reset()
|
||||
}
|
||||
|
||||
if let me = authenticationBox.authentication.account() {
|
||||
let isMyself = (author == me)
|
||||
let isMuting: Bool
|
||||
let isBlocking: Bool
|
||||
|
||||
if let relationship = notification.relationship {
|
||||
isMuting = relationship.muting
|
||||
isBlocking = relationship.blocking || relationship.domainBlocking
|
||||
} else {
|
||||
isMuting = false
|
||||
isBlocking = false
|
||||
}
|
||||
|
||||
let menuContext = NotificationView.AuthorMenuContext(name: metaAuthorName.string, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself)
|
||||
let (menu, actions) = setupAuthorMenu(menuContext: menuContext)
|
||||
menuButton.menu = menu
|
||||
authorActions = actions
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
menuButton.isHidden = menuContext.isMyself
|
||||
}
|
||||
|
||||
timestampUpdatePublisher
|
||||
.prepend(Date())
|
||||
.eraseToAnyPublisher()
|
||||
.sink { [weak self] now in
|
||||
guard let self, let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else { return }
|
||||
|
||||
let formattedTimestamp = now.localizedTimeAgo(since: notification.entity.createdAt)
|
||||
dateLabel.configure(content: PlaintextMetaContent(string: formattedTimestamp))
|
||||
|
||||
self.accessibilityLabel = [
|
||||
"\(author.displayNameWithFallback) \(type)",
|
||||
author.acct,
|
||||
formattedTimestamp
|
||||
].joined(separator: ", ")
|
||||
if self.statusView.isHidden == false {
|
||||
self.accessibilityLabel! += ", " + (self.statusView.accessibilityLabel ?? "")
|
||||
}
|
||||
if self.quoteStatusViewContainerView.isHidden == false {
|
||||
self.accessibilityLabel! += ", " + (self.quoteStatusView.accessibilityLabel ?? "")
|
||||
}
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
switch notification.followRequestState.state {
|
||||
case .isAccept:
|
||||
self.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
||||
self.acceptFollowRequestButton.isUserInteractionEnabled = false
|
||||
self.acceptFollowRequestButton.setImage(nil, for: .normal)
|
||||
self.acceptFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.accepted, for: .normal)
|
||||
case .isReject:
|
||||
self.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
||||
self.rejectFollowRequestButton.isUserInteractionEnabled = false
|
||||
self.rejectFollowRequestButton.setImage(nil, for: .normal)
|
||||
self.rejectFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.rejected, for: .normal)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let state = notification.transientFollowRequestState.state
|
||||
if state == .isAccepting {
|
||||
self.acceptFollowRequestActivityIndicatorView.startAnimating()
|
||||
self.acceptFollowRequestButton.tintColor = .clear
|
||||
self.acceptFollowRequestButton.setTitleColor(.clear, for: .normal)
|
||||
} else {
|
||||
self.acceptFollowRequestActivityIndicatorView.stopAnimating()
|
||||
self.acceptFollowRequestButton.tintColor = .white
|
||||
self.acceptFollowRequestButton.setTitleColor(.white, for: .normal)
|
||||
}
|
||||
if state == .isRejecting {
|
||||
self.rejectFollowRequestActivityIndicatorView.startAnimating()
|
||||
self.rejectFollowRequestButton.tintColor = .clear
|
||||
self.rejectFollowRequestButton.setTitleColor(.clear, for: .normal)
|
||||
} else {
|
||||
self.rejectFollowRequestActivityIndicatorView.stopAnimating()
|
||||
self.rejectFollowRequestButton.tintColor = .black
|
||||
self.rejectFollowRequestButton.setTitleColor(.black, for: .normal)
|
||||
}
|
||||
|
||||
if state == .isAccept {
|
||||
self.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
||||
}
|
||||
if state == .isReject {
|
||||
self.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import Meta
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
public protocol NotificationViewDelegate: AnyObject {
|
||||
func notificationView(_ notificationView: NotificationView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
@ -47,12 +48,6 @@ public final class NotificationView: UIView {
|
||||
var notificationActions = [UIAccessibilityCustomAction]()
|
||||
var authorActions = [UIAccessibilityCustomAction]()
|
||||
|
||||
public private(set) lazy var viewModel: ViewModel = {
|
||||
let viewModel = ViewModel()
|
||||
viewModel.bind(notificationView: self)
|
||||
return viewModel
|
||||
}()
|
||||
|
||||
let containerStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
@ -175,14 +170,16 @@ public final class NotificationView: UIView {
|
||||
public let quoteStatusViewContainerView = UIView()
|
||||
public let quoteBackgroundView = UIView()
|
||||
public let quoteStatusView = StatusView()
|
||||
|
||||
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
public func prepareForReuse() {
|
||||
disposeBag.removeAll()
|
||||
|
||||
viewModel.objects.removeAll()
|
||||
|
||||
viewModel.authContext = nil
|
||||
viewModel.authorAvatarImageURL = nil
|
||||
avatarButton.avatarImageView.image = nil
|
||||
avatarButton.avatarImageView.cancelTask()
|
||||
|
||||
authorContainerViewBottomPaddingView.isHidden = true
|
||||
@ -478,8 +475,14 @@ extension NotificationView: AdaptiveContainerView {
|
||||
}
|
||||
|
||||
extension NotificationView {
|
||||
public typealias AuthorMenuContext = StatusAuthorView.AuthorMenuContext
|
||||
|
||||
public struct AuthorMenuContext {
|
||||
public let name: String
|
||||
public let isMuting: Bool
|
||||
public let isBlocking: Bool
|
||||
public let isMyself: Bool
|
||||
}
|
||||
|
||||
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) {
|
||||
var actions: [[MastodonMenu.Action]] = []
|
||||
var upperActions: [MastodonMenu.Action] = []
|
@ -21,7 +21,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
var viewModel: NotificationViewModel!
|
||||
var viewModel: NotificationViewModel?
|
||||
|
||||
let pageSegmentedControl = UISegmentedControl()
|
||||
|
||||
@ -38,7 +38,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency {
|
||||
animated: animated
|
||||
)
|
||||
|
||||
viewModel.currentPageIndex = index
|
||||
viewModel?.currentPageIndex = index
|
||||
}
|
||||
|
||||
}
|
||||
@ -49,7 +49,7 @@ extension NotificationViewController {
|
||||
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
|
||||
setupSegmentedControl(scopes: viewModel.scopes)
|
||||
setupSegmentedControl(scopes: APIService.MastodonNotificationScope.allCases)
|
||||
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationItem.titleView = pageSegmentedControl
|
||||
NSLayoutConstraint.activate([
|
||||
@ -58,7 +58,7 @@ extension NotificationViewController {
|
||||
pageSegmentedControl.addTarget(self, action: #selector(NotificationViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged)
|
||||
|
||||
dataSource = viewModel
|
||||
viewModel.$viewControllers
|
||||
viewModel?.$viewControllers
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] viewControllers in
|
||||
guard let self = self else { return }
|
||||
@ -68,11 +68,11 @@ extension NotificationViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.viewControllers = viewModel.scopes.map { scope in
|
||||
viewModel?.viewControllers = APIService.MastodonNotificationScope.allCases.map { scope in
|
||||
createViewController(for: scope)
|
||||
}
|
||||
|
||||
viewModel.$currentPageIndex
|
||||
viewModel?.$currentPageIndex
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] currentPageIndex in
|
||||
guard let self = self else { return }
|
||||
@ -127,7 +127,7 @@ extension NotificationViewController {
|
||||
}
|
||||
|
||||
// set initial selection
|
||||
guard !pageSegmentedControl.isSelected else { return }
|
||||
guard let viewModel, !pageSegmentedControl.isSelected else { return }
|
||||
if viewModel.currentPageIndex < pageSegmentedControl.numberOfSegments {
|
||||
pageSegmentedControl.selectedSegmentIndex = viewModel.currentPageIndex
|
||||
} else {
|
||||
@ -136,12 +136,13 @@ extension NotificationViewController {
|
||||
}
|
||||
|
||||
private func createViewController(for scope: NotificationTimelineViewModel.Scope) -> UIViewController {
|
||||
guard let authContext = viewModel?.authContext else { return UITableViewController() }
|
||||
let viewController = NotificationTimelineViewController()
|
||||
viewController.context = context
|
||||
viewController.coordinator = coordinator
|
||||
viewController.viewModel = NotificationTimelineViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
authContext: authContext,
|
||||
scope: scope
|
||||
)
|
||||
return viewController
|
||||
|
@ -22,7 +22,6 @@ final class NotificationViewModel {
|
||||
let viewDidLoad = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
let scopes = NotificationTimelineViewModel.Scope.allCases
|
||||
@Published var viewControllers: [UIViewController] = []
|
||||
@Published var currentPageIndex = 0 {
|
||||
didSet {
|
||||
|
@ -139,30 +139,22 @@ class MastodonLoginViewController: UIViewController, NeedsDependency {
|
||||
|
||||
@objc func login() {
|
||||
guard let server = viewModel.selectedServer else { return }
|
||||
|
||||
|
||||
authenticationViewModel
|
||||
.authenticated
|
||||
.asyncMap { domain, user -> Result<Bool, Error> in
|
||||
do {
|
||||
let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
|
||||
return .success(result)
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isActived):
|
||||
assert(isActived)
|
||||
self.coordinator.setup()
|
||||
.authenticated.sink { (domain, account) in
|
||||
Task { @MainActor in
|
||||
do {
|
||||
_ = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: account.id)
|
||||
FileManager.default.store(account: account, forUserID: MastodonUserIdentifier(domain: domain, userID: account.id))
|
||||
|
||||
self.coordinator.setup()
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
authenticationViewModel.isAuthenticating.send(true)
|
||||
context.apiService.createApplication(domain: server.domain)
|
||||
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||
|
@ -187,7 +187,6 @@ extension AuthenticationViewModel {
|
||||
userToken: Mastodon.Entity.Token
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
|
||||
let managedObjectContext = context.backgroundManagedObjectContext
|
||||
|
||||
return context.apiService.accountVerifyCredentials(
|
||||
domain: info.domain,
|
||||
@ -195,23 +194,21 @@ extension AuthenticationViewModel {
|
||||
)
|
||||
.tryMap { response -> Mastodon.Response.Content<Mastodon.Entity.Account> in
|
||||
let account = response.value
|
||||
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
|
||||
mastodonUserRequest.fetchLimit = 1
|
||||
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
|
||||
throw AuthenticationError.badCredentials
|
||||
}
|
||||
|
||||
|
||||
let authentication = MastodonAuthentication.createFrom(domain: info.domain,
|
||||
userID: account.id,
|
||||
username: account.username,
|
||||
appAccessToken: userToken.accessToken, // TODO: swap app token
|
||||
userAccessToken: userToken.accessToken,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret)
|
||||
|
||||
AuthenticationServiceProvider.shared
|
||||
.authentications
|
||||
.insert(MastodonAuthentication.createFrom(domain: info.domain,
|
||||
userID: mastodonUser.id,
|
||||
username: mastodonUser.username,
|
||||
appAccessToken: userToken.accessToken, // TODO: swap app token
|
||||
userAccessToken: userToken.accessToken,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret), at: 0)
|
||||
|
||||
.insert(authentication, at: 0)
|
||||
|
||||
FileManager.default.store(account: account, forUserID: authentication.userIdentifier())
|
||||
|
||||
return response
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -19,7 +19,7 @@ final class ProfileAboutViewModel {
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published var user: MastodonUser?
|
||||
@Published var account: Mastodon.Entity.Account
|
||||
@Published var isEditing = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
|
||||
@ -32,25 +32,13 @@ final class ProfileAboutViewModel {
|
||||
@Published var emojiMeta: MastodonContent.Emojis = [:]
|
||||
@Published var createdAt: Date = Date()
|
||||
|
||||
init(context: AppContext) {
|
||||
init(context: AppContext, account: Mastodon.Entity.Account) {
|
||||
self.account = account
|
||||
self.context = context
|
||||
// end init
|
||||
|
||||
$user
|
||||
.compactMap { $0 }
|
||||
.flatMap { $0.publisher(for: \.emojis) }
|
||||
.map { $0.asDictionary }
|
||||
.assign(to: &$emojiMeta)
|
||||
|
||||
$user
|
||||
.compactMap { $0 }
|
||||
.flatMap { $0.publisher(for: \.fields) }
|
||||
.assign(to: &$fields)
|
||||
|
||||
$user
|
||||
.compactMap { $0 }
|
||||
.flatMap { $0.publisher(for: \.createdAt) }
|
||||
.assign(to: &$createdAt)
|
||||
emojiMeta = account.emojiMeta
|
||||
fields = account.mastodonFields
|
||||
createdAt = account.createdAt
|
||||
|
||||
Publishers.CombineLatest(
|
||||
$fields,
|
||||
|
@ -58,7 +58,7 @@ extension FavoriteViewModel.State {
|
||||
Task {
|
||||
// reset
|
||||
await viewModel.dataController.reset()
|
||||
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
@ -18,6 +18,7 @@ import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
import TabBarPager
|
||||
import MastodonSDK
|
||||
|
||||
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
||||
func profileHeaderViewController(_ profileHeaderViewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||
@ -29,12 +30,12 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi
|
||||
static let segmentedControlHeight: CGFloat = 50
|
||||
static let headerMinHeight: CGFloat = segmentedControlHeight
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var context: AppContext!
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: ProfileHeaderViewModel!
|
||||
|
||||
let viewModel: ProfileHeaderViewModel
|
||||
|
||||
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||
weak var headerDelegate: TabBarPagerHeaderDelegate?
|
||||
|
||||
@ -51,7 +52,7 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi
|
||||
return titleView
|
||||
}()
|
||||
|
||||
let profileHeaderView = ProfileHeaderView()
|
||||
let profileHeaderView: ProfileHeaderView
|
||||
|
||||
// private var isBannerPinned = false
|
||||
|
||||
@ -81,14 +82,29 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi
|
||||
return documentPickerController
|
||||
}()
|
||||
|
||||
|
||||
}
|
||||
init(context: AppContext, authContext: AuthContext, coordinator: SceneCoordinator, profileViewModel: ProfileViewModel) {
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
self.viewModel = ProfileHeaderViewModel(context: context, authContext: authContext, account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship)
|
||||
self.profileHeaderView = ProfileHeaderView(account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship)
|
||||
|
||||
extension ProfileHeaderViewController {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
viewModel.$account
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] account in
|
||||
guard let self else { return }
|
||||
|
||||
self.profileHeaderView.configuration(account: account)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
view.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
@ -128,17 +144,11 @@ extension ProfileHeaderViewController {
|
||||
self.titleView.subtitleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.$user
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] user in
|
||||
guard let self = self else { return }
|
||||
guard let user = user else { return }
|
||||
self.profileHeaderView.prepareForReuse()
|
||||
self.profileHeaderView.configuration(user: user)
|
||||
}
|
||||
viewModel.$relationship
|
||||
.assign(to: \.relationship, on: profileHeaderView.viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.$relationshipActionOptionSet
|
||||
.assign(to: \.relationshipActionOptionSet, on: profileHeaderView.viewModel)
|
||||
viewModel.$account
|
||||
.assign(to: \.account, on: profileHeaderView.viewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.$isMyself
|
||||
.assign(to: \.isMyself, on: profileHeaderView.viewModel)
|
||||
@ -263,41 +273,35 @@ extension ProfileHeaderViewController {
|
||||
profileHeaderView.avatarButton.alpha = alpha
|
||||
profileHeaderView.editAvatarBackgroundView.alpha = alpha
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ProfileHeaderViewDelegate
|
||||
extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) {
|
||||
guard let user = viewModel.user else { return }
|
||||
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
|
||||
|
||||
Task {
|
||||
try await DataSourceFacade.coordinateToMediaPreviewScene(
|
||||
dependency: self,
|
||||
user: record,
|
||||
account: viewModel.account,
|
||||
previewContext: DataSourceFacade.ImagePreviewContext(
|
||||
imageView: button.avatarImageView,
|
||||
containerView: .profileAvatar(profileHeaderView)
|
||||
)
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) {
|
||||
guard let user = viewModel.user else { return }
|
||||
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
|
||||
|
||||
Task {
|
||||
try await DataSourceFacade.coordinateToMediaPreviewScene(
|
||||
dependency: self,
|
||||
user: record,
|
||||
account: viewModel.account,
|
||||
previewContext: DataSourceFacade.ImagePreviewContext(
|
||||
imageView: imageView,
|
||||
containerView: .profileBanner(profileHeaderView)
|
||||
)
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
func profileHeaderView(
|
||||
@ -329,37 +333,37 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
|
||||
switch meter {
|
||||
case .post:
|
||||
// do nothing
|
||||
break
|
||||
case .follower:
|
||||
guard let domain = viewModel.user?.domain,
|
||||
let userID = viewModel.user?.id
|
||||
else { return }
|
||||
let followerListViewModel = FollowerListViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
domain: domain,
|
||||
userID: userID
|
||||
)
|
||||
_ = coordinator.present(
|
||||
scene: .follower(viewModel: followerListViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
case .following:
|
||||
guard let domain = viewModel.user?.domain,
|
||||
let userID = viewModel.user?.id
|
||||
else { return }
|
||||
let followingListViewModel = FollowingListViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
domain: domain,
|
||||
userID: userID
|
||||
)
|
||||
_ = coordinator.present(
|
||||
scene: .following(viewModel: followingListViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
break
|
||||
case .follower:
|
||||
guard let domain = viewModel.account.domain else { return }
|
||||
let userID = viewModel.account.id
|
||||
let followerListViewModel = FollowerListViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
domain: domain,
|
||||
userID: userID
|
||||
)
|
||||
_ = coordinator.present(
|
||||
scene: .follower(viewModel: followerListViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
|
||||
case .following:
|
||||
guard let domain = viewModel.account.domain else { return }
|
||||
|
||||
let userID = viewModel.account.id
|
||||
let followingListViewModel = FollowingListViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
domain: domain,
|
||||
userID: userID
|
||||
)
|
||||
_ = coordinator.present(
|
||||
scene: .following(viewModel: followingListViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,8 +26,9 @@ final class ProfileHeaderViewModel {
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
|
||||
@Published var user: MastodonUser?
|
||||
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
|
||||
@Published var me: Mastodon.Entity.Account
|
||||
@Published var account: Mastodon.Entity.Account
|
||||
@Published var relationship: Mastodon.Entity.Relationship?
|
||||
|
||||
@Published var isMyself = false
|
||||
@Published var isEditing = false
|
||||
@ -44,10 +45,13 @@ final class ProfileHeaderViewModel {
|
||||
@Published var isTitleViewDisplaying = false
|
||||
@Published var isTitleViewContentOffsetSet = false
|
||||
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
||||
self.account = account
|
||||
self.me = me
|
||||
self.relationship = relationship
|
||||
|
||||
$accountForEdit
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] account in
|
||||
|
@ -7,49 +7,19 @@
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension ProfileHeaderView {
|
||||
func configuration(user: MastodonUser) {
|
||||
// header
|
||||
user.publisher(for: \.header)
|
||||
.map { _ in user.headerImageURL() }
|
||||
.assign(to: \.headerImageURL, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// avatar
|
||||
user.publisher(for: \.avatar)
|
||||
.map { _ in user.avatarImageURL() }
|
||||
.assign(to: \.avatarImageURL, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// emojiMeta
|
||||
user.publisher(for: \.emojis)
|
||||
.map { $0.asDictionary }
|
||||
.assign(to: \.emojiMeta, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// name
|
||||
user.publisher(for: \.displayName)
|
||||
.map { _ in user.displayNameWithFallback }
|
||||
.assign(to: \.name, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// username
|
||||
viewModel.acct = user.acctWithDomain
|
||||
// bio
|
||||
user.publisher(for: \.note)
|
||||
.assign(to: \.note, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// dashboard
|
||||
user.publisher(for: \.statusesCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.statusesCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
user.publisher(for: \.followingCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.followingCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
user.publisher(for: \.followersCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.followersCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
func configuration(account: Mastodon.Entity.Account) {
|
||||
viewModel.headerImageURL = account.headerImageURL()
|
||||
viewModel.avatarImageURL = account.avatarImageURL()
|
||||
viewModel.emojiMeta = account.emojiMeta
|
||||
viewModel.name = account.displayNameWithFallback
|
||||
viewModel.acct = account.acctWithDomain
|
||||
viewModel.note = account.note
|
||||
viewModel.statusesCount = account.statusesCount
|
||||
viewModel.followingCount = account.followingCount
|
||||
viewModel.followersCount = account.followersCount
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension ProfileHeaderView {
|
||||
class ViewModel: ObservableObject {
|
||||
@ -45,15 +46,16 @@ extension ProfileHeaderView {
|
||||
|
||||
@Published var fields: [MastodonField] = []
|
||||
|
||||
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
|
||||
@Published var me: Mastodon.Entity.Account
|
||||
@Published var account: Mastodon.Entity.Account
|
||||
@Published var relationship: Mastodon.Entity.Relationship?
|
||||
@Published var isRelationshipActionButtonHidden = false
|
||||
@Published var isMyself = false
|
||||
|
||||
init() {
|
||||
$relationshipActionOptionSet
|
||||
.compactMap { $0.highPriorityAction(except: []) }
|
||||
.map { $0 == .none }
|
||||
.assign(to: &$isRelationshipActionButtonHidden)
|
||||
init(account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) {
|
||||
self.account = account
|
||||
self.me = me
|
||||
self.relationship = relationship
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,13 +98,16 @@ extension ProfileHeaderView.ViewModel {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// follows you
|
||||
$relationshipActionOptionSet
|
||||
.map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
|
||||
Publishers.CombineLatest($relationship, $isMyself)
|
||||
.map { relationship, isMyself in
|
||||
return (relationship?.followedBy ?? false) && (isMyself == false)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { isFollowingBy in
|
||||
view.followsYouBlurEffectView.isHidden = !isFollowingBy
|
||||
.sink { followsYou in
|
||||
view.followsYouBlurEffectView.isHidden = (followsYou == false)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// avatar
|
||||
Publishers.CombineLatest4(
|
||||
$avatarImageURL,
|
||||
@ -118,8 +123,12 @@ extension ProfileHeaderView.ViewModel {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// blur for blocking & blockingBy
|
||||
$relationshipActionOptionSet
|
||||
.map { $0.contains(.blocking) || $0.contains(.blockingBy) || $0.contains(.domainBlocking) }
|
||||
$relationship
|
||||
.compactMap { relationship in
|
||||
guard let relationship else { return false }
|
||||
|
||||
return relationship.blocking || relationship.blockedBy || relationship.domainBlocking
|
||||
}
|
||||
.sink { needsImageOverlayBlurred in
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
|
||||
@ -182,17 +191,26 @@ extension ProfileHeaderView.ViewModel {
|
||||
view.bioMetaText.configure(content: metaContent)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
$relationshipActionOptionSet
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { optionSet in
|
||||
let isBlocking = optionSet.contains(.blocking) || optionSet.contains(.domainBlocking)
|
||||
let isBlockedBy = optionSet.contains(.blockingBy)
|
||||
let isSuspended = optionSet.contains(.suspended)
|
||||
|
||||
Publishers.CombineLatest($relationship, $account)
|
||||
.compactMap { relationship, account in
|
||||
|
||||
guard let relationship else { return nil }
|
||||
|
||||
let isBlocking = relationship.blocking || relationship.domainBlocking
|
||||
let isBlockedBy = relationship.blockedBy
|
||||
let isSuspended = account.suspended ?? false
|
||||
|
||||
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
|
||||
|
||||
return isNeedsHidden
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { isNeedsHidden in
|
||||
view.bioMetaText.textView.isHidden = isNeedsHidden
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// dashboard
|
||||
$isMyself
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -243,22 +261,20 @@ extension ProfileHeaderView.ViewModel {
|
||||
.store(in: &disposeBag)
|
||||
// relationship
|
||||
$isRelationshipActionButtonHidden
|
||||
.assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer)
|
||||
.assign(to: \.isHidden, on: view.relationshipActionButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
$relationshipActionOptionSet,
|
||||
Publishers.CombineLatest3($me, $account, $relationship).eraseToAnyPublisher(),
|
||||
$isEditing,
|
||||
$isUpdating
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { relationshipActionOptionSet, isEditing, isUpdating in
|
||||
if relationshipActionOptionSet.contains(.edit) {
|
||||
// check .edit state and set .editing when isEditing
|
||||
view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
|
||||
view.configure(state: isEditing ? .editing : .normal)
|
||||
} else {
|
||||
view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet)
|
||||
}
|
||||
.sink { tuple, isEditing, isUpdating in
|
||||
let (me, account, relationship) = tuple
|
||||
guard let relationship else { return }
|
||||
|
||||
view.relationshipActionButton.configure(relationship: relationship, between: account, and: me, isEditing: isEditing, isUpdating: isUpdating)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
protocol ProfileHeaderViewDelegate: AnyObject {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton)
|
||||
@ -42,12 +43,8 @@ final class ProfileHeaderView: UIView {
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
private(set) lazy var viewModel: ViewModel = {
|
||||
let viewModel = ViewModel()
|
||||
viewModel.bind(view: self)
|
||||
return viewModel
|
||||
}()
|
||||
|
||||
private(set) var viewModel: ViewModel
|
||||
|
||||
let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
let bannerContainerView = UIView()
|
||||
let bannerImageView: UIImageView = {
|
||||
@ -197,7 +194,6 @@ final class ProfileHeaderView: UIView {
|
||||
|
||||
let statusDashboardView = ProfileStatusDashboardView()
|
||||
|
||||
let relationshipActionButtonShadowContainer = ShadowBackgroundContainer()
|
||||
let relationshipActionButton: ProfileRelationshipActionButton = {
|
||||
let button = ProfileRelationshipActionButton()
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
@ -238,20 +234,14 @@ final class ProfileHeaderView: UIView {
|
||||
return metaText
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
init(account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) {
|
||||
|
||||
viewModel = ViewModel(account: account, me: me, relationship: relationship)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
viewModel.bind(view: self)
|
||||
|
||||
extension ProfileHeaderView {
|
||||
private func _init() {
|
||||
setColors()
|
||||
|
||||
// banner
|
||||
@ -378,7 +368,7 @@ extension ProfileHeaderView {
|
||||
avatarImageViewBackgroundView.bottomAnchor.constraint(equalTo: dashboardContainer.bottomAnchor),
|
||||
])
|
||||
|
||||
// authorContainer: H - [ nameContainer | padding | relationshipActionButtonShadowContainer ]
|
||||
// authorContainer: H - [ nameContainer | padding | relationshipActionButton ]
|
||||
let authorContainer = UIStackView()
|
||||
authorContainer.axis = .horizontal
|
||||
authorContainer.alignment = .top
|
||||
@ -429,11 +419,9 @@ extension ProfileHeaderView {
|
||||
|
||||
authorContainer.addArrangedSubview(nameContainerStackView)
|
||||
authorContainer.addArrangedSubview(UIView())
|
||||
authorContainer.addArrangedSubview(relationshipActionButtonShadowContainer)
|
||||
|
||||
authorContainer.addArrangedSubview(relationshipActionButton)
|
||||
|
||||
relationshipActionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
relationshipActionButtonShadowContainer.addSubview(relationshipActionButton)
|
||||
relationshipActionButton.pinToParent()
|
||||
NSLayoutConstraint.activate([
|
||||
relationshipActionButton.widthAnchor.constraint(greaterThanOrEqualToConstant: ProfileHeaderView.friendshipActionButtonSize.width).priority(.required - 1),
|
||||
relationshipActionButton.heightAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.height).priority(.defaultHigh),
|
||||
@ -460,6 +448,8 @@ extension ProfileHeaderView {
|
||||
|
||||
updateLayoutMargins()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setColors() {
|
||||
backgroundColor = .systemBackground
|
||||
@ -542,27 +532,3 @@ extension ProfileHeaderView: ProfileStatusDashboardViewDelegate {
|
||||
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, dashboardMeterViewDidPressed: dashboardMeterView, meter: meter)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct ProfileHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
let banner = ProfileHeaderView()
|
||||
banner.bannerImageView.image = UIImage(named: "lucas-ludwig")
|
||||
return banner
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 800))
|
||||
UIViewPreview(width: 375) {
|
||||
let banner = ProfileHeaderView()
|
||||
//banner.bannerImageView.image = UIImage(named: "peter-luo")
|
||||
return banner
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.previewLayout(.fixed(width: 375, height: 800))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -1,56 +0,0 @@
|
||||
//
|
||||
// MeProfileViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
final class MeProfileViewModel: ProfileViewModel {
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
|
||||
super.init(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
optionalMastodonUser: user
|
||||
)
|
||||
|
||||
$me
|
||||
.sink { [weak self] me in
|
||||
guard let self = self else { return }
|
||||
self.user = me
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
Task {
|
||||
do {
|
||||
|
||||
_ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
|
||||
|
||||
try await context.managedObjectContext.performChanges {
|
||||
guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
self.me = me
|
||||
}
|
||||
} catch {
|
||||
// do nothing?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -119,10 +119,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
|
||||
private(set) lazy var tabBarPagerController = TabBarPagerController()
|
||||
|
||||
private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = {
|
||||
let viewController = ProfileHeaderViewController()
|
||||
viewController.context = context
|
||||
viewController.coordinator = coordinator
|
||||
viewController.viewModel = ProfileHeaderViewModel(context: context, authContext: viewModel.authContext)
|
||||
let viewController = ProfileHeaderViewController(context: context, authContext: authContext, coordinator: coordinator, profileViewModel: viewModel)
|
||||
return viewController
|
||||
}()
|
||||
|
||||
@ -169,6 +166,8 @@ extension ProfileViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ProfileViewController.relationshipChanged(_:)), name: .relationshipChanged, object: nil)
|
||||
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
if isModal {
|
||||
@ -202,33 +201,38 @@ extension ProfileViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest4 (
|
||||
viewModel.relationshipViewModel.$isSuspended,
|
||||
// build items
|
||||
Publishers.CombineLatest4(
|
||||
viewModel.$relationship,
|
||||
profileHeaderViewController.viewModel.$isTitleViewDisplaying,
|
||||
editingAndUpdatingPublisher.eraseToAnyPublisher(),
|
||||
barButtonItemHiddenPublisher.eraseToAnyPublisher()
|
||||
editingAndUpdatingPublisher,
|
||||
barButtonItemHiddenPublisher
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isSuspended, isTitleViewDisplaying, tuple1, tuple2 in
|
||||
guard let self = self else { return }
|
||||
.sink { [weak self] account, isTitleViewDisplaying, tuple1, tuple2 in
|
||||
guard let self else { return }
|
||||
let (isEditing, _) = tuple1
|
||||
let (isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden) = tuple2
|
||||
|
||||
var items: [UIBarButtonItem] = []
|
||||
defer {
|
||||
self.navigationItem.rightBarButtonItems = !items.isEmpty ? items : nil
|
||||
if items.isNotEmpty {
|
||||
self.navigationItem.rightBarButtonItems = items
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItems = nil
|
||||
}
|
||||
}
|
||||
|
||||
guard !isSuspended else {
|
||||
if let suspended = self.viewModel.account.suspended, suspended == true {
|
||||
return
|
||||
}
|
||||
|
||||
guard !isEditing else {
|
||||
guard isEditing == false else {
|
||||
items.append(self.cancelEditingBarButtonItem)
|
||||
return
|
||||
}
|
||||
|
||||
guard !isTitleViewDisplaying else {
|
||||
guard isTitleViewDisplaying == false else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -280,8 +284,6 @@ extension ProfileViewController {
|
||||
bindTitleView()
|
||||
bindMoreBarButtonItem()
|
||||
bindPager()
|
||||
|
||||
viewModel.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -292,7 +294,8 @@ extension ProfileViewController {
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.viewDidAppear.send()
|
||||
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
|
||||
@ -302,9 +305,9 @@ extension ProfileViewController {
|
||||
|
||||
private func bindViewModel() {
|
||||
// header
|
||||
let headerViewModel = profileHeaderViewController.viewModel!
|
||||
viewModel.$user
|
||||
.assign(to: \.user, on: headerViewModel)
|
||||
let headerViewModel = profileHeaderViewController.viewModel
|
||||
viewModel.$account
|
||||
.assign(to: \.account, on: headerViewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.$isEditing
|
||||
.assign(to: \.isEditing, on: headerViewModel)
|
||||
@ -312,33 +315,39 @@ extension ProfileViewController {
|
||||
viewModel.$isUpdating
|
||||
.assign(to: \.isUpdating, on: headerViewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.relationshipViewModel.$isMyself
|
||||
.assign(to: \.isMyself, on: headerViewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.relationshipViewModel.$optionSet
|
||||
.map { $0 ?? .none }
|
||||
.assign(to: \.relationshipActionOptionSet, on: headerViewModel)
|
||||
viewModel.$relationship
|
||||
.assign(to: \.relationship, on: headerViewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.$accountForEdit
|
||||
.assign(to: \.accountForEdit, on: headerViewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// timeline
|
||||
|
||||
[
|
||||
viewModel.postsUserTimelineViewModel,
|
||||
viewModel.repliesUserTimelineViewModel,
|
||||
viewModel.mediaUserTimelineViewModel,
|
||||
].forEach { userTimelineViewModel in
|
||||
viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag)
|
||||
viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag)
|
||||
viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag)
|
||||
viewModel.relationshipViewModel.$isDomainBlocking.assign(to: \.isDomainBlocking, on: userTimelineViewModel).store(in: &disposeBag)
|
||||
|
||||
viewModel.relationship.publisher
|
||||
.map { $0.blocking }
|
||||
.assign(to: \UserTimelineViewModel.isBlocking, on: userTimelineViewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.relationship.publisher
|
||||
.compactMap { $0.blockedBy }
|
||||
.assign(to: \UserTimelineViewModel.isBlockedBy, on: userTimelineViewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.$account
|
||||
.compactMap { $0.suspended }
|
||||
.assign(to: \UserTimelineViewModel.isSuspended, on: userTimelineViewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
// about
|
||||
let aboutViewModel = viewModel.profileAboutViewModel
|
||||
viewModel.$user
|
||||
.assign(to: \.user, on: aboutViewModel)
|
||||
viewModel.$account
|
||||
.assign(to: \.account, on: aboutViewModel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.$isEditing
|
||||
.assign(to: \.isEditing, on: aboutViewModel)
|
||||
@ -380,45 +389,53 @@ extension ProfileViewController {
|
||||
self.navigationItem.title = name
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
Publishers.CombineLatest(
|
||||
profileHeaderViewController.viewModel.$user,
|
||||
profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear
|
||||
)
|
||||
.sink { [weak self] (user, _) in
|
||||
guard let self, let user else { return }
|
||||
Task {
|
||||
_ = try await self.context.apiService.fetchUser(
|
||||
username: user.username,
|
||||
domain: user.domainFromAcct,
|
||||
authenticationBox: self.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
|
||||
guard let self else { return }
|
||||
let account = self.viewModel.account
|
||||
guard let domain = account.domainFromAcct else { return }
|
||||
Task {
|
||||
let account = try await self.context.apiService.fetchUser(
|
||||
username: account.username,
|
||||
domain: domain,
|
||||
authenticationBox: self.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
guard let account else { return }
|
||||
|
||||
let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: self.authContext.mastodonAuthenticationBox).value.first
|
||||
|
||||
guard let relationship else { return }
|
||||
|
||||
self.viewModel.relationship = relationship
|
||||
self.viewModel.account = account
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func bindMoreBarButtonItem() {
|
||||
Publishers.CombineLatest(
|
||||
viewModel.$user,
|
||||
viewModel.relationshipViewModel.$optionSet
|
||||
viewModel.$account,
|
||||
viewModel.$relationship
|
||||
)
|
||||
.asyncMap { [weak self] user, relationshipSet -> UIMenu? in
|
||||
guard let self, let user else { return nil }
|
||||
.asyncMap { [weak self] user, relationship -> UIMenu? in
|
||||
guard let self, let relationship, let domain = user.domainFromAcct else { return nil }
|
||||
|
||||
let name = user.displayNameWithFallback
|
||||
let domain = user.domainFromAcct
|
||||
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
|
||||
var menuActions: [MastodonMenu.Action] = [
|
||||
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
|
||||
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
|
||||
.blockDomain(.init(domain: domain, isBlocking: self.viewModel.relationshipViewModel.isDomainBlocking)),
|
||||
.muteUser(.init(name: name, isMuting: relationship.muting)),
|
||||
.blockUser(.init(name: name, isBlocking: relationship.blocking)),
|
||||
.blockDomain(.init(domain: domain, isBlocking: relationship.domainBlocking)),
|
||||
.reportUser(.init(name: name)),
|
||||
.shareUser(.init(name: name)),
|
||||
]
|
||||
|
||||
if let me = self.viewModel?.me, me.following.contains(user) {
|
||||
let showReblogs = me.showingReblogsBy.contains(user)
|
||||
if relationship.following {
|
||||
let showReblogs = relationship.showingReblogs// me.showingReblogsBy.contains(user)
|
||||
let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs)
|
||||
menuActions.insert(.hideReblogs(context), at: 1)
|
||||
}
|
||||
@ -480,26 +497,6 @@ extension ProfileViewController {
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
// private func bindProfileRelationship() {
|
||||
//
|
||||
// Publishers.CombineLatest3(
|
||||
// viewModel.isBlocking.eraseToAnyPublisher(),
|
||||
// viewModel.isBlockedBy.eraseToAnyPublisher(),
|
||||
// viewModel.suspended.eraseToAnyPublisher()
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] isBlocking, isBlockedBy, suspended in
|
||||
// guard let self = self else { return }
|
||||
// let isNeedSetHidden = isBlocking || isBlockedBy || suspended
|
||||
// self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden
|
||||
// self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden
|
||||
// self.profileHeaderViewController.viewModel.needsFiledCollectionViewHidden.value = isNeedSetHidden
|
||||
// self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isNeedSetHidden
|
||||
// self.viewModel.needsPagePinToTop.value = isNeedSetHidden
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// } // end func bindProfileRelationship
|
||||
|
||||
private func handleMetaPress(_ meta: Meta) {
|
||||
switch meta {
|
||||
case .url(_, _, let url, _):
|
||||
@ -532,24 +529,19 @@ extension ProfileViewController {
|
||||
}
|
||||
|
||||
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
guard let user = viewModel.user else { return }
|
||||
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
|
||||
Task {
|
||||
let _activityViewController = try await DataSourceFacade.createActivityViewController(
|
||||
dependency: self,
|
||||
user: record
|
||||
)
|
||||
guard let activityViewController = _activityViewController else { return }
|
||||
_ = self.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: nil,
|
||||
barButtonItem: sender
|
||||
),
|
||||
from: self,
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
} // end Task
|
||||
let activityViewController = DataSourceFacade.createActivityViewController(
|
||||
dependency: self,
|
||||
account: viewModel.account
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: nil,
|
||||
barButtonItem: sender
|
||||
),
|
||||
from: self,
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
@ -563,8 +555,8 @@ extension ProfileViewController {
|
||||
}
|
||||
|
||||
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
guard let mastodonUser = viewModel.user else { return }
|
||||
let mention = "@" + mastodonUser.acct
|
||||
|
||||
let mention = "@" + viewModel.account.acct
|
||||
UITextChecker.learnWord(mention)
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
@ -586,11 +578,24 @@ extension ProfileViewController {
|
||||
userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
|
||||
}
|
||||
|
||||
// trigger authenticated user account update
|
||||
viewModel.context.authenticationService.updateActiveUserAccountPublisher.send()
|
||||
Task {
|
||||
let account = viewModel.account
|
||||
if let domain = account.domain,
|
||||
let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox),
|
||||
let updatedRelationship = try? await context.apiService.relationship(forAccounts: [updatedAccount], authenticationBox: authContext.mastodonAuthenticationBox).value.first
|
||||
{
|
||||
viewModel.account = updatedAccount
|
||||
viewModel.relationship = updatedRelationship
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
sender.endRefreshing()
|
||||
if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value {
|
||||
viewModel.me = updatedMe
|
||||
FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier())
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
sender.endRefreshing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -690,34 +695,6 @@ extension ProfileViewController: TabBarPagerDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
//// MARK: - UIScrollViewDelegate
|
||||
//extension ProfileViewController: UIScrollViewDelegate {
|
||||
//
|
||||
// func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
// contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
|
||||
// let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
|
||||
// if scrollView.contentOffset.y < topMaxContentOffsetY {
|
||||
// self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
|
||||
// for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
|
||||
// postTimelineView.scrollView?.contentOffset.y = 0
|
||||
// }
|
||||
// contentOffsets.removeAll()
|
||||
// } else {
|
||||
// containerScrollView.contentOffset.y = topMaxContentOffsetY
|
||||
// if viewModel.needsPagePinToTop.value {
|
||||
// // do nothing
|
||||
// } else {
|
||||
// if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
|
||||
// let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
|
||||
// customScrollViewContainerController.scrollView?.contentOffset.y = contentOffsetY
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
// MARK: - AuthContextProvider
|
||||
extension ProfileViewController: AuthContextProvider {
|
||||
var authContext: AuthContext { viewModel.authContext }
|
||||
@ -730,60 +707,65 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
||||
profileHeaderView: ProfileHeaderView,
|
||||
relationshipButtonDidPressed button: ProfileRelationshipActionButton
|
||||
) {
|
||||
let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none
|
||||
|
||||
// handle edit logic for editable profile
|
||||
// handle relationship logic for non-editable profile
|
||||
if relationshipActionSet.contains(.edit) {
|
||||
// do nothing when updating
|
||||
guard !viewModel.isUpdating else { return }
|
||||
if viewModel.me == viewModel.account {
|
||||
editProfile()
|
||||
} else {
|
||||
editRelationship()
|
||||
}
|
||||
}
|
||||
|
||||
guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return }
|
||||
guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return }
|
||||
|
||||
let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited
|
||||
|
||||
if isEdited {
|
||||
// update profile when edited
|
||||
viewModel.isUpdating = true
|
||||
Task { @MainActor in
|
||||
do {
|
||||
// TODO: handle error
|
||||
_ = try await viewModel.updateProfileInfo(
|
||||
headerProfileInfo: profileHeaderViewModel.profileInfoEditing,
|
||||
aboutProfileInfo: profileAboutViewModel.profileInfoEditing
|
||||
)
|
||||
self.viewModel.isEditing = false
|
||||
|
||||
} catch {
|
||||
let alertController = UIAlertController(
|
||||
for: error,
|
||||
title: L10n.Common.Alerts.EditProfileFailure.title,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
|
||||
alertController.addAction(okAction)
|
||||
self.present(alertController, animated: true)
|
||||
|
||||
private func editProfile() {
|
||||
// do nothing when updating
|
||||
guard !viewModel.isUpdating else { return }
|
||||
|
||||
let profileHeaderViewModel = profileHeaderViewController.viewModel
|
||||
guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return }
|
||||
|
||||
let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited
|
||||
|
||||
if isEdited {
|
||||
// update profile when edited
|
||||
viewModel.isUpdating = true
|
||||
Task { @MainActor in
|
||||
do {
|
||||
// TODO: handle error
|
||||
let updatedAccount = try await viewModel.updateProfileInfo(
|
||||
headerProfileInfo: profileHeaderViewModel.profileInfoEditing,
|
||||
aboutProfileInfo: profileAboutViewModel.profileInfoEditing
|
||||
).value
|
||||
self.viewModel.isEditing = false
|
||||
self.viewModel.account = updatedAccount
|
||||
|
||||
} catch {
|
||||
let alertController = UIAlertController(
|
||||
for: error,
|
||||
title: L10n.Common.Alerts.EditProfileFailure.title,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
|
||||
alertController.addAction(okAction)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
// finish updating
|
||||
self.viewModel.isUpdating = false
|
||||
}
|
||||
} else {
|
||||
// set `updating` then toggle `edit` state
|
||||
viewModel.isUpdating = true
|
||||
viewModel.fetchEditProfileInfo()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
defer {
|
||||
// finish updating
|
||||
self.viewModel.isUpdating = false
|
||||
}
|
||||
|
||||
// finish updating
|
||||
self.viewModel.isUpdating = false
|
||||
} // end Task
|
||||
} else {
|
||||
// set `updating` then toggle `edit` state
|
||||
viewModel.isUpdating = true
|
||||
viewModel.fetchEditProfileInfo()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
defer {
|
||||
// finish updating
|
||||
self.viewModel.isUpdating = false
|
||||
}
|
||||
switch completion {
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
|
||||
alertController.addAction(okAction)
|
||||
_ = self.coordinator.present(
|
||||
scene: .alertController(alertController: alertController),
|
||||
@ -793,101 +775,105 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
||||
case .finished:
|
||||
// enter editing mode
|
||||
self.viewModel.isEditing.toggle()
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.accountForEdit = response.value
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
} else {
|
||||
guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
|
||||
switch relationshipAction {
|
||||
case .none:
|
||||
break
|
||||
case .follow, .request, .pending, .following:
|
||||
guard let user = viewModel.user else { return }
|
||||
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.accountForEdit = response.value
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
private func editRelationship() {
|
||||
guard let relationship = viewModel.relationship else { return }
|
||||
|
||||
let account = viewModel.account
|
||||
|
||||
viewModel.isUpdating = true
|
||||
|
||||
if relationship.blocking {
|
||||
let name = account.displayNameWithFallback
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
_ = try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: self,
|
||||
user: record
|
||||
account: account
|
||||
)
|
||||
}
|
||||
case .muting:
|
||||
guard let user = viewModel.user else { return }
|
||||
let name = user.displayNameWithFallback
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserMuteAction(
|
||||
dependency: self,
|
||||
user: record
|
||||
)
|
||||
}
|
||||
}
|
||||
alertController.addAction(unmuteAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
case .domainBlocking:
|
||||
guard let user = viewModel.user else { return }
|
||||
let domain = user.domainFromAcct
|
||||
}
|
||||
alertController.addAction(unblockAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true))
|
||||
} else if relationship.domainBlocking {
|
||||
guard let domain = account.domain else { return }
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(domain),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
Task {
|
||||
try await DataSourceFacade.responseToDomainBlockAction(dependency: self, user: record)
|
||||
}
|
||||
}
|
||||
alertController.addAction(unblockAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(domain),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
case .blocking:
|
||||
guard let user = viewModel.user else { return }
|
||||
let name = user.displayNameWithFallback
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: self,
|
||||
user: record
|
||||
)
|
||||
}
|
||||
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Actions.unblockDomain(domain), style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
_ = try await DataSourceFacade.responseToDomainBlockAction(dependency: self, account: account)
|
||||
|
||||
guard let newRelationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: self.authContext.mastodonAuthenticationBox).value.first else { return }
|
||||
|
||||
self.viewModel.isUpdating = false
|
||||
|
||||
// we need to trigger this here as domain block doesn't return a relationship
|
||||
let userInfo = [
|
||||
UserInfoKey.relationship: newRelationship,
|
||||
]
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo)
|
||||
}
|
||||
alertController.addAction(unblockAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating:
|
||||
break
|
||||
}
|
||||
alertController.addAction(unblockAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true))
|
||||
|
||||
} else if relationship.muting {
|
||||
let name = account.displayNameWithFallback
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
_ = try await DataSourceFacade.responseToUserMuteAction(dependency: self, account: account)
|
||||
}
|
||||
}
|
||||
alertController.addAction(unmuteAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true))
|
||||
} else {
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
_ = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: self,
|
||||
account: viewModel.account
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func profileHeaderViewController(
|
||||
_ profileHeaderViewController: ProfileHeaderViewController,
|
||||
profileHeaderView: ProfileHeaderView,
|
||||
@ -913,23 +899,44 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate {
|
||||
// MARK: - MastodonMenuDelegate
|
||||
extension ProfileViewController: MastodonMenuDelegate {
|
||||
func menuAction(_ action: MastodonMenu.Action) {
|
||||
guard let user = viewModel.user else { return }
|
||||
switch action {
|
||||
case .muteUser(_),
|
||||
.blockUser(_),
|
||||
.blockDomain(_),
|
||||
.hideReblogs(_):
|
||||
Task {
|
||||
try await DataSourceFacade.responseToMenuAction(
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: DataSourceFacade.MenuContext(
|
||||
author: viewModel.account,
|
||||
statusViewModel: nil,
|
||||
button: nil,
|
||||
barButtonItem: self.moreMenuBarButtonItem
|
||||
))
|
||||
}
|
||||
case .reportUser(_), .shareUser(_):
|
||||
Task {
|
||||
try await DataSourceFacade.responseToMenuAction(
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: DataSourceFacade.MenuContext(
|
||||
author: viewModel.account,
|
||||
statusViewModel: nil,
|
||||
button: nil,
|
||||
barButtonItem: self.moreMenuBarButtonItem
|
||||
))
|
||||
}
|
||||
|
||||
let userRecord: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
|
||||
|
||||
Task {
|
||||
try await DataSourceFacade.responseToMenuAction(
|
||||
dependency: self,
|
||||
action: action,
|
||||
menuContext: DataSourceFacade.MenuContext(
|
||||
author: userRecord,
|
||||
authorEntity: nil,
|
||||
statusViewModel: nil,
|
||||
button: nil,
|
||||
barButtonItem: self.moreMenuBarButtonItem
|
||||
)
|
||||
)
|
||||
} // end Task
|
||||
case .translateStatus(_),
|
||||
.showOriginal,
|
||||
.bookmarkStatus(_),
|
||||
.shareStatus,
|
||||
.deleteStatus,
|
||||
.editStatus,
|
||||
.followUser(_):
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -987,3 +994,44 @@ extension ProfileViewController: DataSourceProvider {
|
||||
viewModel.mediaUserTimelineViewModel.dataController.update(status: status, intent: intent)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notifications
|
||||
|
||||
extension ProfileViewController {
|
||||
@objc
|
||||
func relationshipChanged(_ notification: Notification) {
|
||||
|
||||
guard let userInfo = notification.userInfo, let relationship = userInfo[UserInfoKey.relationship] as? Mastodon.Entity.Relationship else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.isUpdating = true
|
||||
if viewModel.account.id == relationship.id {
|
||||
// if relationship belongs to an other account
|
||||
Task {
|
||||
let account = viewModel.account
|
||||
if let domain = account.domain,
|
||||
let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) {
|
||||
viewModel.account = updatedAccount
|
||||
|
||||
viewModel.relationship = relationship
|
||||
self.profileHeaderViewController.viewModel.relationship = relationship
|
||||
self.profileHeaderViewController.profileHeaderView.viewModel.relationship = relationship
|
||||
}
|
||||
|
||||
viewModel.isUpdating = false
|
||||
}
|
||||
} else if viewModel.account == viewModel.me {
|
||||
// update my profile
|
||||
Task {
|
||||
if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value {
|
||||
viewModel.me = updatedMe
|
||||
viewModel.account = updatedMe
|
||||
FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier())
|
||||
}
|
||||
|
||||
viewModel.isUpdating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,18 +33,17 @@ class ProfileViewModel: NSObject {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
@Published var me: MastodonUser?
|
||||
@Published var user: MastodonUser?
|
||||
|
||||
|
||||
@Published var me: Mastodon.Entity.Account
|
||||
@Published var account: Mastodon.Entity.Account
|
||||
@Published var relationship: Mastodon.Entity.Relationship?
|
||||
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
@Published var isEditing = false
|
||||
@Published var isUpdating = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
|
||||
// output
|
||||
let relationshipViewModel = RelationshipViewModel()
|
||||
|
||||
@Published var userIdentifier: UserIdentifier? = nil
|
||||
|
||||
@Published var isRelationshipActionButtonHidden: Bool = true
|
||||
@ -57,10 +56,13 @@ class ProfileViewModel: NSObject {
|
||||
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||
init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, me: Mastodon.Entity.Account) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.user = mastodonUser
|
||||
self.account = account
|
||||
self.relationship = relationship
|
||||
self.me = me
|
||||
|
||||
self.postsUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
@ -79,69 +81,65 @@ class ProfileViewModel: NSObject {
|
||||
title: L10n.Scene.Profile.SegmentedControl.media,
|
||||
queryFilter: .init(onlyMedia: true)
|
||||
)
|
||||
self.profileAboutViewModel = ProfileAboutViewModel(context: context)
|
||||
self.profileAboutViewModel = ProfileAboutViewModel(context: context, account: account)
|
||||
super.init()
|
||||
|
||||
// bind me
|
||||
self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
|
||||
$me
|
||||
.assign(to: \.me, on: relationshipViewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind user
|
||||
$user
|
||||
.map { user -> UserIdentifier? in
|
||||
guard let user = user else { return nil }
|
||||
return MastodonUserIdentifier(domain: user.domain, userID: user.id)
|
||||
}
|
||||
.assign(to: &$userIdentifier)
|
||||
$user
|
||||
.assign(to: \.user, on: relationshipViewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
if let domain = account.domain {
|
||||
userIdentifier = MastodonUserIdentifier(domain: domain, userID: account.id)
|
||||
} else {
|
||||
userIdentifier = nil
|
||||
}
|
||||
|
||||
// bind userIdentifier
|
||||
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
|
||||
$userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
|
||||
$userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
|
||||
|
||||
// bind bar button items
|
||||
relationshipViewModel.$optionSet
|
||||
.sink { [weak self] optionSet in
|
||||
guard let self = self else { return }
|
||||
guard let optionSet = optionSet, !optionSet.contains(.none) else {
|
||||
self.isReplyBarButtonItemHidden = true
|
||||
self.isMoreMenuBarButtonItemHidden = true
|
||||
self.isMeBarButtonItemsHidden = true
|
||||
Publishers.CombineLatest3($account, $me, $relationship)
|
||||
.sink(receiveValue: { [weak self] account, me, relationship in
|
||||
guard let self else {
|
||||
self?.isReplyBarButtonItemHidden = true
|
||||
self?.isMoreMenuBarButtonItemHidden = true
|
||||
self?.isMeBarButtonItemsHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
let isMyself = optionSet.contains(.isMyself)
|
||||
|
||||
let isMyself = (account == me)
|
||||
self.isReplyBarButtonItemHidden = isMyself
|
||||
self.isMoreMenuBarButtonItemHidden = isMyself
|
||||
self.isMeBarButtonItemsHidden = !isMyself
|
||||
}
|
||||
self.isMeBarButtonItemsHidden = (isMyself == false)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewDidAppear
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
self.isReplyBarButtonItemHidden = self.isReplyBarButtonItemHidden
|
||||
self.isMoreMenuBarButtonItemHidden = self.isMoreMenuBarButtonItemHidden
|
||||
self.isMeBarButtonItemsHidden = self.isMeBarButtonItemsHidden
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// query relationship
|
||||
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
|
||||
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
|
||||
}
|
||||
|
||||
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
|
||||
|
||||
// observe friendship
|
||||
Publishers.CombineLatest(
|
||||
userRecord,
|
||||
$account,
|
||||
pendingRetryPublisher
|
||||
)
|
||||
.sink { [weak self] userRecord, _ in
|
||||
guard let self = self else { return }
|
||||
guard let userRecord = userRecord else { return }
|
||||
.sink { [weak self] account, _ in
|
||||
guard let self else { return }
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try await self.updateRelationship(
|
||||
record: userRecord,
|
||||
let response = try await self.context.apiService.relationship(
|
||||
forAccounts: [account],
|
||||
authenticationBox: self.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
// there are seconds delay after request follow before requested -> following. Query again when needs
|
||||
guard let relationship = response.value.first else { return }
|
||||
if relationship.requested == true {
|
||||
@ -157,11 +155,12 @@ class ProfileViewModel: NSObject {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
let isBlockingOrBlocked = Publishers.CombineLatest(
|
||||
relationshipViewModel.$isBlocking,
|
||||
relationshipViewModel.$isBlockingBy
|
||||
let isBlockingOrBlocked = Publishers.CombineLatest3(
|
||||
(relationship?.blocking ?? false).publisher,
|
||||
(relationship?.blockedBy ?? false).publisher,
|
||||
(relationship?.domainBlocking ?? false).publisher
|
||||
)
|
||||
.map { $0 || $1 }
|
||||
.map { $0 || $1 || $2 }
|
||||
.share()
|
||||
|
||||
Publishers.CombineLatest(
|
||||
@ -172,33 +171,20 @@ class ProfileViewModel: NSObject {
|
||||
.assign(to: &$isPagingEnabled)
|
||||
}
|
||||
|
||||
|
||||
func viewDidLoad() {
|
||||
|
||||
}
|
||||
|
||||
// fetch profile info before edit
|
||||
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||
guard let me else {
|
||||
guard let domain = me.domain else {
|
||||
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let mastodonAuthentication = authContext.mastodonAuthenticationBox.authentication
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
|
||||
return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization)
|
||||
return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization)
|
||||
.tryMap { response in
|
||||
FileManager.default.store(account: response.value, forUserID: mastodonAuthentication.userIdentifier())
|
||||
return response
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func updateRelationship(
|
||||
record: ManagedObjectRecord<MastodonUser>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
|
||||
let response = try await context.apiService.relationship(
|
||||
records: [record],
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileViewModel {
|
||||
@ -242,10 +228,14 @@ extension ProfileViewModel {
|
||||
source: nil,
|
||||
fieldsAttributes: fieldsAttributes
|
||||
)
|
||||
return try await context.apiService.accountUpdateCredentials(
|
||||
let response = try await context.apiService.accountUpdateCredentials(
|
||||
domain: domain,
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
|
||||
FileManager.default.store(account: response.value, forUserID: authenticationBox.authentication.userIdentifier())
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
@ -1,132 +0,0 @@
|
||||
//
|
||||
// RemoteProfileViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
final class RemoteProfileViewModel: ProfileViewModel {
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) {
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
|
||||
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
let authorization = authContext.mastodonAuthenticationBox.userAuthorization
|
||||
Just(userID)
|
||||
.asyncMap { userID in
|
||||
try await context.apiService.accountInfo(
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
authorization: authorization
|
||||
)
|
||||
}
|
||||
.retry(3)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(_):
|
||||
// TODO: handle error
|
||||
break
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id)
|
||||
guard let mastodonUser = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
self.user = mastodonUser
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) {
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
|
||||
|
||||
Task { @MainActor in
|
||||
let response = try await context.apiService.notification(
|
||||
notificationID: notificationID,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
let userID = response.value.account.id
|
||||
|
||||
let _user: MastodonUser? = try await context.managedObjectContext.perform {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
|
||||
request.fetchLimit = 1
|
||||
return context.managedObjectContext.safeFetch(request).first
|
||||
}
|
||||
|
||||
if let user = _user {
|
||||
self.user = user
|
||||
} else {
|
||||
_ = try await context.apiService.accountInfo(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
userID: userID,
|
||||
authorization: authContext.mastodonAuthenticationBox.userAuthorization
|
||||
)
|
||||
|
||||
let _user: MastodonUser? = try await context.managedObjectContext.perform {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
|
||||
request.fetchLimit = 1
|
||||
return context.managedObjectContext.safeFetch(request).first
|
||||
}
|
||||
|
||||
self.user = _user
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authContext: AuthContext, acct: String){
|
||||
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
|
||||
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
let authenticationBox = authContext.mastodonAuthenticationBox
|
||||
|
||||
Just(acct)
|
||||
.asyncMap { acct -> Mastodon.Response.Content<Mastodon.Entity.Account?> in
|
||||
try await context.apiService.search(
|
||||
query: .init(q: acct, type: .accounts, resolve: true),
|
||||
authenticationBox: authenticationBox
|
||||
).map { $0.accounts.first }
|
||||
}
|
||||
.retry(3)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(_):
|
||||
// TODO: handle error
|
||||
break
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self, let value = response.value else { return }
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: value.id)
|
||||
guard let mastodonUser = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
self.user = mastodonUser
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
@ -117,7 +117,7 @@ extension UserTimelineViewModel.State {
|
||||
|
||||
Task {
|
||||
let maxID = await viewModel.dataController.records.last?.id
|
||||
|
||||
|
||||
guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
|
@ -20,7 +20,7 @@ extension RebloggedByViewController: DataSourceProvider {
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
switch item {
|
||||
case .account(let account, let relationship):
|
||||
return .account(account: account, relationship: relationship)
|
||||
|
@ -41,6 +41,7 @@ extension UserListViewModel {
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||
|
||||
snapshot.appendSections([.main])
|
||||
|
||||
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
|
||||
|
@ -30,7 +30,7 @@ extension UserListViewModel {
|
||||
extension UserListViewModel.State {
|
||||
class Initial: UserListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let _ = viewModel else { return false }
|
||||
guard viewModel != nil else { return false }
|
||||
switch stateClass {
|
||||
case is Reloading.Type:
|
||||
return true
|
||||
@ -75,8 +75,8 @@ extension UserListViewModel.State {
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
guard viewModel != nil, let stateMachine else { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
@ -118,10 +118,11 @@ extension UserListViewModel.State {
|
||||
maxID = nil
|
||||
}
|
||||
|
||||
guard let viewModel = viewModel else { return }
|
||||
guard let viewModel else { return }
|
||||
|
||||
let maxID = self.maxID
|
||||
|
||||
let authenticationBox = viewModel.authContext.mastodonAuthenticationBox
|
||||
|
||||
Task {
|
||||
do {
|
||||
let accountResponse: Mastodon.Response.Content<[Mastodon.Entity.Account]>
|
||||
@ -130,13 +131,13 @@ extension UserListViewModel.State {
|
||||
accountResponse = try await viewModel.context.apiService.favoritedBy(
|
||||
status: status,
|
||||
query: .init(maxID: maxID, limit: nil),
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
case .rebloggedBy(let status):
|
||||
accountResponse = try await viewModel.context.apiService.rebloggedBy(
|
||||
status: status,
|
||||
query: .init(maxID: maxID, limit: nil),
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,19 +20,22 @@ class ReportViewController: UIViewController, NeedsDependency, ReportViewControl
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: ReportViewModel!
|
||||
|
||||
let viewModel: ReportViewModel
|
||||
|
||||
lazy var cancelBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(ReportViewController.cancelBarButtonItemDidPressed(_:))
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
init(viewModel: ReportViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
extension ReportViewController {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -46,11 +49,10 @@ extension ReportViewController {
|
||||
viewModel.reportStatusViewModel.delegate = self
|
||||
viewModel.reportSupplementaryViewModel.delegate = self
|
||||
|
||||
let reportReasonViewController = ReportReasonViewController()
|
||||
let reportReasonViewController = ReportReasonViewController(viewModel: viewModel.reportReasonViewModel)
|
||||
reportReasonViewController.context = context
|
||||
reportReasonViewController.coordinator = coordinator
|
||||
reportReasonViewController.viewModel = viewModel.reportReasonViewModel
|
||||
|
||||
|
||||
addChild(reportReasonViewController)
|
||||
reportReasonViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(reportReasonViewController.view)
|
||||
@ -58,10 +60,6 @@ extension ReportViewController {
|
||||
reportReasonViewController.view.pinToParent()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportViewController {
|
||||
|
||||
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
@ -84,7 +82,8 @@ extension ReportViewController: ReportReasonViewControllerDelegate {
|
||||
let reportResultViewModel = ReportResultViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
user: viewModel.user,
|
||||
account: viewModel.account,
|
||||
relationship: viewModel.relationship,
|
||||
isReported: false
|
||||
)
|
||||
_ = coordinator.present(
|
||||
@ -156,11 +155,12 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let _ = try await viewModel.report()
|
||||
|
||||
|
||||
let reportResultViewModel = ReportResultViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
user: viewModel.user,
|
||||
account: viewModel.account,
|
||||
relationship: viewModel.relationship,
|
||||
isReported: true
|
||||
)
|
||||
|
||||
|
@ -28,7 +28,8 @@ class ReportViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let account: Mastodon.Entity.Account
|
||||
let relationship: Mastodon.Entity.Relationship
|
||||
let status: MastodonStatus?
|
||||
|
||||
// output
|
||||
@ -39,17 +40,19 @@ class ReportViewModel {
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
account: Mastodon.Entity.Account,
|
||||
relationship: Mastodon.Entity.Relationship,
|
||||
status: MastodonStatus?
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.user = user
|
||||
self.account = account
|
||||
self.relationship = relationship
|
||||
self.status = status
|
||||
self.reportReasonViewModel = ReportReasonViewModel(context: context)
|
||||
self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context)
|
||||
self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, user: user, status: status)
|
||||
self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, user: user)
|
||||
self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, account: account, status: status)
|
||||
self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, account: account)
|
||||
// end init
|
||||
|
||||
// setup reason viewModel
|
||||
@ -57,17 +60,8 @@ class ReportViewModel {
|
||||
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisPost
|
||||
} else {
|
||||
Task { @MainActor in
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let _username: String? = try? await managedObjectContext.perform {
|
||||
let user = user.object(in: managedObjectContext)
|
||||
return user?.acctWithDomain
|
||||
}
|
||||
if let username = _username {
|
||||
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(username)
|
||||
} else {
|
||||
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisAccount
|
||||
}
|
||||
} // end Task
|
||||
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(account.username)
|
||||
}
|
||||
}
|
||||
|
||||
// bind server rules
|
||||
@ -96,73 +90,63 @@ extension ReportViewModel {
|
||||
func report() async throws {
|
||||
guard !isReporting else { return }
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform {
|
||||
guard let user = self.user.object(in: managedObjectContext) else { return nil }
|
||||
|
||||
// the status picker is essential step in report flow
|
||||
// only check isSkip or not
|
||||
let statusIDs: [MastodonStatus.ID]? = {
|
||||
if self.reportStatusViewModel.isSkip {
|
||||
let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in
|
||||
return record.id
|
||||
}
|
||||
return _id.flatMap { [$0] } ?? []
|
||||
} else {
|
||||
return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in
|
||||
return record.id
|
||||
}
|
||||
let account = self.account
|
||||
// the status picker is essential step in report flow
|
||||
// only check isSkip or not
|
||||
let statusIDs: [MastodonStatus.ID]? = {
|
||||
if self.reportStatusViewModel.isSkip {
|
||||
let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in
|
||||
return record.id
|
||||
}
|
||||
}()
|
||||
|
||||
// the user comment is essential step in report flow
|
||||
// only check isSkip or not
|
||||
let comment: String? = {
|
||||
let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
|
||||
if let comment = _comment, !comment.isEmpty {
|
||||
return comment
|
||||
} else {
|
||||
return nil
|
||||
return _id.flatMap { [$0] } ?? []
|
||||
} else {
|
||||
return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in
|
||||
return record.id
|
||||
}
|
||||
}()
|
||||
return Mastodon.API.Reports.FileReportQuery(
|
||||
accountID: user.id,
|
||||
statusIDs: statusIDs,
|
||||
comment: comment,
|
||||
forward: true,
|
||||
category: {
|
||||
switch self.reportReasonViewModel.selectReason {
|
||||
}
|
||||
}()
|
||||
|
||||
// the user comment is essential step in report flow
|
||||
// only check isSkip or not
|
||||
let comment: String? = {
|
||||
let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
|
||||
if let comment = _comment, !comment.isEmpty {
|
||||
return comment
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
let query = Mastodon.API.Reports.FileReportQuery(
|
||||
accountID: account.id,
|
||||
statusIDs: statusIDs,
|
||||
comment: comment,
|
||||
forward: true,
|
||||
category: {
|
||||
switch self.reportReasonViewModel.selectReason {
|
||||
case .dislike: return nil
|
||||
case .spam: return .spam
|
||||
case .violateRule: return .violation
|
||||
case .other: return .other
|
||||
case .none: return nil
|
||||
}
|
||||
}(),
|
||||
ruleIDs: {
|
||||
switch self.reportReasonViewModel.selectReason {
|
||||
}
|
||||
}(),
|
||||
ruleIDs: {
|
||||
switch self.reportReasonViewModel.selectReason {
|
||||
case .violateRule:
|
||||
let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted()
|
||||
return ruleIDs
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
guard let query = _query else { return }
|
||||
}
|
||||
}()
|
||||
)
|
||||
|
||||
do {
|
||||
isReporting = true
|
||||
#if DEBUG
|
||||
try await Task.sleep(nanoseconds: .second * 3)
|
||||
#else
|
||||
let _ = try await context.apiService.report(
|
||||
query: query,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
#endif
|
||||
isReportSuccess = true
|
||||
} catch {
|
||||
isReporting = false
|
||||
|
@ -25,9 +25,9 @@ final class ReportReasonViewController: UIViewController, NeedsDependency, Repor
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
var viewModel: ReportReasonViewModel!
|
||||
private(set) lazy var reportReasonView = ReportReasonView(viewModel: viewModel)
|
||||
|
||||
let viewModel: ReportReasonViewModel
|
||||
let reportReasonView: ReportReasonView
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
@ -35,10 +35,14 @@ final class ReportReasonViewController: UIViewController, NeedsDependency, Repor
|
||||
return navigationActionView
|
||||
}()
|
||||
|
||||
init(viewModel: ReportReasonViewModel) {
|
||||
self.viewModel = viewModel
|
||||
reportReasonView = ReportReasonView(viewModel: viewModel)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportReasonViewController {
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -75,7 +75,7 @@ struct ReportResultView: View {
|
||||
action: {
|
||||
viewModel.followActionPublisher.send()
|
||||
},
|
||||
title: viewModel.relationshipViewModel.isFollowing ? L10n.Scene.Report.StepFinal.unfollow : L10n.Scene.Report.StepFinal.unfollowed,
|
||||
title: viewModel.relationship.following ? L10n.Scene.Report.StepFinal.unfollow : L10n.Scene.Report.StepFinal.unfollowed,
|
||||
isBusy: viewModel.isRequestFollow
|
||||
)
|
||||
}
|
||||
@ -92,7 +92,7 @@ struct ReportResultView: View {
|
||||
action: {
|
||||
viewModel.muteActionPublisher.send()
|
||||
},
|
||||
title: viewModel.relationshipViewModel.isMuting ? L10n.Common.Controls.Friendship.muted : L10n.Common.Controls.Friendship.mute,
|
||||
title: viewModel.relationship.muting ? L10n.Common.Controls.Friendship.muted : L10n.Common.Controls.Friendship.mute,
|
||||
isBusy: viewModel.isRequestMute
|
||||
)
|
||||
}
|
||||
@ -109,7 +109,7 @@ struct ReportResultView: View {
|
||||
action: {
|
||||
viewModel.blockActionPublisher.send()
|
||||
},
|
||||
title: viewModel.relationshipViewModel.isBlocking ? L10n.Common.Controls.Friendship.blocked : L10n.Common.Controls.Friendship.block,
|
||||
title: viewModel.relationship.blocking ? L10n.Common.Controls.Friendship.blocked : L10n.Common.Controls.Friendship.block,
|
||||
isBusy: viewModel.isRequestBlock
|
||||
)
|
||||
}
|
||||
|
@ -88,10 +88,11 @@ extension ReportResultViewController {
|
||||
guard !self.viewModel.isRequestFollow else { return }
|
||||
self.viewModel.isRequestFollow = true
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
let newRelationship = try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: self,
|
||||
user: self.viewModel.user
|
||||
account: self.viewModel.account
|
||||
)
|
||||
self.viewModel.relationship = newRelationship
|
||||
} catch {
|
||||
// handle error
|
||||
}
|
||||
@ -108,10 +109,11 @@ extension ReportResultViewController {
|
||||
guard !self.viewModel.isRequestMute else { return }
|
||||
self.viewModel.isRequestMute = true
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserMuteAction(
|
||||
let newRelationship = try await DataSourceFacade.responseToUserMuteAction(
|
||||
dependency: self,
|
||||
user: self.viewModel.user
|
||||
account: self.viewModel.account
|
||||
)
|
||||
self.viewModel.relationship = newRelationship
|
||||
} catch {
|
||||
// handle error
|
||||
}
|
||||
@ -128,10 +130,11 @@ extension ReportResultViewController {
|
||||
guard !self.viewModel.isRequestBlock else { return }
|
||||
self.viewModel.isRequestBlock = true
|
||||
do {
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
let newRelationship = try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: self,
|
||||
user: self.viewModel.user
|
||||
account: self.viewModel.account
|
||||
)
|
||||
self.viewModel.relationship = newRelationship
|
||||
} catch {
|
||||
// handle error
|
||||
}
|
||||
|
@ -23,7 +23,8 @@ class ReportResultViewModel: ObservableObject {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let account: Mastodon.Entity.Account
|
||||
var relationship: Mastodon.Entity.Relationship
|
||||
let isReported: Bool
|
||||
|
||||
var headline: String {
|
||||
@ -39,8 +40,7 @@ class ReportResultViewModel: ObservableObject {
|
||||
// output
|
||||
@Published var avatarURL: URL?
|
||||
@Published var username: String = ""
|
||||
|
||||
let relationshipViewModel = RelationshipViewModel()
|
||||
|
||||
let muteActionPublisher = PassthroughSubject<Void, Never>()
|
||||
let followActionPublisher = PassthroughSubject<Void, Never>()
|
||||
let blockActionPublisher = PassthroughSubject<Void, Never>()
|
||||
@ -48,24 +48,22 @@ class ReportResultViewModel: ObservableObject {
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
account: Mastodon.Entity.Account,
|
||||
relationship: Mastodon.Entity.Relationship,
|
||||
isReported: Bool
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.user = user
|
||||
self.account = account
|
||||
self.relationship = relationship
|
||||
self.isReported = isReported
|
||||
// end init
|
||||
|
||||
Task { @MainActor in
|
||||
guard let user = user.object(in: context.managedObjectContext) else { return }
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return }
|
||||
self.relationshipViewModel.user = user
|
||||
self.relationshipViewModel.me = me
|
||||
|
||||
self.avatarURL = user.avatarImageURL()
|
||||
self.username = user.acctWithDomain
|
||||
|
||||
self.avatarURL = account.avatarImageURL()
|
||||
self.username = account.username
|
||||
|
||||
} // end Task
|
||||
}
|
||||
|
||||
|
@ -68,19 +68,9 @@ extension ReportStatusViewModel.State {
|
||||
Task {
|
||||
let maxID = await viewModel.dataController.records.last?.id
|
||||
|
||||
let managedObjectContext = viewModel.context.managedObjectContext
|
||||
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
|
||||
guard let user = viewModel.user.object(in: managedObjectContext) else { return nil }
|
||||
return user.id
|
||||
}
|
||||
guard let userID = _userID else {
|
||||
await enter(state: Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.userTimeline(
|
||||
accountID: userID,
|
||||
accountID: viewModel.account.id,
|
||||
maxID: maxID,
|
||||
sinceID: nil,
|
||||
excludeReplies: true,
|
||||
|
@ -24,7 +24,7 @@ class ReportStatusViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let account: Mastodon.Entity.Account
|
||||
let status: MastodonStatus?
|
||||
let dataController: StatusDataController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
@ -52,12 +52,12 @@ class ReportStatusViewModel {
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
account: Mastodon.Entity.Account,
|
||||
status: MastodonStatus?
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.user = user
|
||||
self.account = account
|
||||
self.status = status
|
||||
self.dataController = StatusDataController()
|
||||
// end init
|
||||
|
@ -18,7 +18,7 @@ class ReportSupplementaryViewModel {
|
||||
// Input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let account: Mastodon.Entity.Account
|
||||
let commentContext = ReportItem.CommentContext()
|
||||
|
||||
@Published var isSkip = false
|
||||
@ -31,11 +31,11 @@ class ReportSupplementaryViewModel {
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
account: Mastodon.Entity.Account
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.user = user
|
||||
self.account = account
|
||||
// end init
|
||||
|
||||
Publishers.CombineLatest(
|
||||
|
@ -1,140 +0,0 @@
|
||||
//
|
||||
// ReportResultActionTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
final class ReportResultActionTableViewCell: UITableViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let containerView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
return stackView
|
||||
}()
|
||||
|
||||
let avatarImageView: AvatarImageView = {
|
||||
let imageView = AvatarImageView()
|
||||
imageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 27)))
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let reportBannerShadowContainer = ShadowBackgroundContainer()
|
||||
let reportBannerLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let padding = Array(repeating: " ", count: 2).joined()
|
||||
label.text = padding + L10n.Scene.Report.reported + padding
|
||||
label.textColor = Asset.Scene.Report.reportBanner.color
|
||||
label.font = FontFamily.Staatliches.regular.font(size: 49)
|
||||
label.backgroundColor = Asset.Scene.Report.background.color
|
||||
label.layer.borderColor = Asset.Scene.Report.reportBanner.color.cgColor
|
||||
label.layer.borderWidth = 6
|
||||
label.layer.masksToBounds = true
|
||||
label.layer.cornerRadius = 12
|
||||
return label
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportResultActionTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
let avatarContainer = UIStackView()
|
||||
avatarContainer.axis = .horizontal
|
||||
containerView.addArrangedSubview(avatarContainer)
|
||||
|
||||
let avatarLeadingPaddingView = UIView()
|
||||
let avatarTrailingPaddingView = UIView()
|
||||
avatarLeadingPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addArrangedSubview(avatarLeadingPaddingView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addArrangedSubview(avatarImageView)
|
||||
avatarTrailingPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addArrangedSubview(avatarTrailingPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: 106).priority(.required - 1),
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: 106).priority(.required - 1),
|
||||
avatarLeadingPaddingView.widthAnchor.constraint(equalTo: avatarTrailingPaddingView.widthAnchor).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
reportBannerShadowContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addSubview(reportBannerShadowContainer)
|
||||
NSLayoutConstraint.activate([
|
||||
reportBannerShadowContainer.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||
reportBannerShadowContainer.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||
])
|
||||
reportBannerShadowContainer.transform = CGAffineTransform(rotationAngle: -(.pi / 180 * 5))
|
||||
|
||||
reportBannerLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
reportBannerShadowContainer.addSubview(reportBannerLabel)
|
||||
reportBannerLabel.pinToParent()
|
||||
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
reportBannerShadowContainer.layer.setupShadow(
|
||||
color: .black,
|
||||
alpha: 0.25,
|
||||
x: 1,
|
||||
y: 0.64,
|
||||
blur: 0.64,
|
||||
spread: 0,
|
||||
roundedRect: reportBannerShadowContainer.bounds,
|
||||
byRoundingCorners: .allCorners,
|
||||
cornerRadii: CGSize(width: 12, height: 12)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
struct ReportResultActionTableViewCell_Preview: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UIViewPreview(width: 375) {
|
||||
let cell = ReportResultActionTableViewCell()
|
||||
cell.avatarImageView.configure(configuration: .init(image: .placeholder(color: .blue)))
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 106))
|
||||
}
|
||||
}
|
||||
#endif
|
@ -11,8 +11,8 @@ import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
protocol ContentSplitViewControllerDelegate: AnyObject {
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: MainTabBarController.Tab)
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: Tab)
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: Tab)
|
||||
}
|
||||
|
||||
final class ContentSplitViewController: UIViewController, NeedsDependency {
|
||||
@ -37,11 +37,11 @@ final class ContentSplitViewController: UIViewController, NeedsDependency {
|
||||
return sidebarViewController
|
||||
}()
|
||||
|
||||
@Published var currentSupplementaryTab: MainTabBarController.Tab = .home
|
||||
@Published var currentSupplementaryTab: Tab = .home
|
||||
private(set) lazy var mainTabBarController: MainTabBarController = {
|
||||
let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext)
|
||||
let mainTabBarController = MainTabBarController(context: self.context, coordinator: self.coordinator, authContext: self.authContext)
|
||||
if let homeTimelineViewController = mainTabBarController.viewController(of: HomeTimelineViewController.self) {
|
||||
homeTimelineViewController.viewModel.displaySettingBarButtonItem = false
|
||||
homeTimelineViewController.viewModel?.displaySettingBarButtonItem = false
|
||||
}
|
||||
return mainTabBarController
|
||||
}()
|
||||
@ -102,7 +102,7 @@ extension ContentSplitViewController {
|
||||
// MARK: - SidebarViewControllerDelegate
|
||||
extension ContentSplitViewController: SidebarViewControllerDelegate {
|
||||
|
||||
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
|
||||
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: Tab) {
|
||||
delegate?.contentSplitViewController(self, sidebarViewController: sidebarViewController, didSelectTab: tab)
|
||||
}
|
||||
|
||||
|
@ -34,105 +34,16 @@ class MainTabBarController: UITabBarController {
|
||||
)
|
||||
|
||||
@Published var currentTab: Tab = .home
|
||||
|
||||
enum Tab: Int, CaseIterable {
|
||||
case home
|
||||
case search
|
||||
case compose
|
||||
case notifications
|
||||
case me
|
||||
|
||||
var tag: Int {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home: return L10n.Common.Controls.Tabs.home
|
||||
case .search: return L10n.Common.Controls.Tabs.searchAndExplore
|
||||
case .compose: return L10n.Common.Controls.Actions.compose
|
||||
case .notifications: return L10n.Common.Controls.Tabs.notifications
|
||||
case .me: return L10n.Common.Controls.Tabs.profile
|
||||
}
|
||||
}
|
||||
let homeTimelineViewController: HomeTimelineViewController
|
||||
let searchViewController: SearchViewController
|
||||
let composeViewController: UIViewController // placeholder
|
||||
let notificationViewController: NotificationViewController
|
||||
let meProfileViewController: ProfileViewController
|
||||
|
||||
var inputLabels: [String]? {
|
||||
switch self {
|
||||
case .home, .compose, .notifications, .me:
|
||||
return nil
|
||||
case .search:
|
||||
return [
|
||||
L10n.Common.Controls.Tabs.A11Y.search,
|
||||
L10n.Common.Controls.Tabs.A11Y.explore,
|
||||
L10n.Common.Controls.Tabs.searchAndExplore
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .home: return UIImage(systemName: "house")!
|
||||
case .search: return UIImage(systemName: "magnifyingglass")!
|
||||
case .compose: return UIImage(systemName: "square.and.pencil")!
|
||||
case .notifications: return UIImage(systemName: "bell")!
|
||||
case .me: return UIImage(systemName: "person")!
|
||||
}
|
||||
}
|
||||
|
||||
var selectedImage: UIImage {
|
||||
return image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
|
||||
var largeImage: UIImage {
|
||||
return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController {
|
||||
guard let authContext = authContext else {
|
||||
return UITableViewController()
|
||||
}
|
||||
|
||||
let viewController: UIViewController
|
||||
switch self {
|
||||
case .home:
|
||||
let _viewController = HomeTimelineViewController()
|
||||
_viewController.context = context
|
||||
_viewController.coordinator = coordinator
|
||||
_viewController.viewModel = .init(context: context, authContext: authContext)
|
||||
viewController = _viewController
|
||||
case .search:
|
||||
let _viewController = SearchViewController()
|
||||
_viewController.context = context
|
||||
_viewController.coordinator = coordinator
|
||||
_viewController.viewModel = .init(context: context, authContext: authContext)
|
||||
viewController = _viewController
|
||||
case .compose:
|
||||
viewController = UIViewController()
|
||||
case .notifications:
|
||||
let _viewController = NotificationViewController()
|
||||
_viewController.context = context
|
||||
_viewController.coordinator = coordinator
|
||||
_viewController.viewModel = .init(context: context, authContext: authContext)
|
||||
viewController = _viewController
|
||||
case .me:
|
||||
let _viewController = ProfileViewController()
|
||||
_viewController.context = context
|
||||
_viewController.coordinator = coordinator
|
||||
_viewController.viewModel = MeProfileViewModel(context: context, authContext: authContext)
|
||||
viewController = _viewController
|
||||
}
|
||||
viewController.title = self.title
|
||||
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
var _viewControllers: [UIViewController] = []
|
||||
|
||||
private(set) var isReadyForWizardAvatarButton = false
|
||||
|
||||
// output
|
||||
var avatarURLObserver: AnyCancellable?
|
||||
@Published var avatarURL: URL?
|
||||
|
||||
// haptic feedback
|
||||
@ -146,15 +57,45 @@ class MainTabBarController: UITabBarController {
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
self.authContext = authContext
|
||||
|
||||
homeTimelineViewController = HomeTimelineViewController()
|
||||
homeTimelineViewController.configureTabBarItem(with: .home)
|
||||
homeTimelineViewController.context = context
|
||||
homeTimelineViewController.coordinator = coordinator
|
||||
|
||||
searchViewController = SearchViewController()
|
||||
searchViewController.configureTabBarItem(with: .search)
|
||||
searchViewController.context = context
|
||||
searchViewController.coordinator = coordinator
|
||||
|
||||
composeViewController = UIViewController()
|
||||
composeViewController.configureTabBarItem(with: .compose)
|
||||
|
||||
notificationViewController = NotificationViewController()
|
||||
notificationViewController.configureTabBarItem(with: .notifications)
|
||||
notificationViewController.context = context
|
||||
notificationViewController.coordinator = coordinator
|
||||
|
||||
meProfileViewController = ProfileViewController()
|
||||
meProfileViewController.context = context
|
||||
meProfileViewController.coordinator = coordinator
|
||||
meProfileViewController.configureTabBarItem(with: .me)
|
||||
|
||||
if let authContext {
|
||||
notificationViewController.viewModel = NotificationViewModel(context: context, authContext: authContext)
|
||||
homeTimelineViewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext)
|
||||
searchViewController.viewModel = SearchViewModel(context: context, authContext: authContext)
|
||||
}
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
viewControllers = [homeTimelineViewController, searchViewController, composeViewController, notificationViewController, meProfileViewController].map { AdaptiveStatusBarStyleNavigationController(rootViewController: $0) }
|
||||
tabBar.addInteraction(largeContentViewerInteraction)
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
|
||||
layoutAvatarButton()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
||||
extension MainTabBarController {
|
||||
@ -171,26 +112,9 @@ extension MainTabBarController {
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
// seealso: `ThemeService.apply(theme:)`
|
||||
let tabs = Tab.allCases
|
||||
var viewControllers = [UIViewController]()
|
||||
|
||||
for tab in tabs {
|
||||
let viewController = tab.viewController(context: context, authContext: authContext, coordinator: coordinator)
|
||||
viewController.tabBarItem.tag = tab.tag
|
||||
viewController.tabBarItem.title = tab.title // needs for acessiblity large content label
|
||||
viewController.tabBarItem.image = tab.image.imageWithoutBaseline()
|
||||
viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
|
||||
viewController.tabBarItem.accessibilityLabel = tab.title
|
||||
viewController.tabBarItem.accessibilityUserInputLabels = tab.inputLabels
|
||||
viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
|
||||
viewControllers.append(viewController)
|
||||
}
|
||||
|
||||
_viewControllers = viewControllers
|
||||
setViewControllers(viewControllers, animated: false)
|
||||
selectedIndex = 0
|
||||
|
||||
|
||||
// hacky workaround for FB11986255 (Setting accessibilityUserInputLabels on a UITabBarItem has no effect)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
|
||||
if let searchItem = self.tabBar.subviews.first(where: { $0.accessibilityLabel == Tab.search.title }) {
|
||||
@ -201,7 +125,7 @@ extension MainTabBarController {
|
||||
context.apiService.error
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] error in
|
||||
guard let self = self, let coordinator = self.coordinator else { return }
|
||||
guard let self, let coordinator = self.coordinator else { return }
|
||||
switch error {
|
||||
case .implicit:
|
||||
break
|
||||
@ -228,15 +152,14 @@ extension MainTabBarController {
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] authentication, currentTab in
|
||||
guard let self = self else { return }
|
||||
guard let notificationViewController = self.notificationViewController else { return }
|
||||
|
||||
guard let self else { return }
|
||||
|
||||
let authentication = self.authContext?.mastodonAuthenticationBox.userAuthorization
|
||||
let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in
|
||||
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.accessToken)
|
||||
return count > 0
|
||||
} ?? false
|
||||
|
||||
|
||||
let image: UIImage
|
||||
if hasUnreadPushNotification {
|
||||
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
|
||||
@ -244,17 +167,16 @@ extension MainTabBarController {
|
||||
} else {
|
||||
image = Tab.notifications.image
|
||||
}
|
||||
|
||||
|
||||
notificationViewController.tabBarItem.image = image.imageWithoutBaseline()
|
||||
notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
layoutAvatarButton()
|
||||
|
||||
|
||||
$avatarURL
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] avatarURL in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
self.avatarButton.avatarImageView.setImage(
|
||||
url: avatarURL,
|
||||
placeholder: .placeholder(color: .systemFill),
|
||||
@ -262,33 +184,28 @@ extension MainTabBarController {
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
NotificationCenter.default.publisher(for: .userFetched)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) {
|
||||
self.avatarURLObserver = user.publisher(for: \.avatar)
|
||||
.sink { [weak self, weak user] _ in
|
||||
guard let self = self else { return }
|
||||
guard let user = user else { return }
|
||||
guard user.managedObjectContext != nil else { return }
|
||||
self.avatarURL = user.avatarImageURL()
|
||||
}
|
||||
|
||||
// a11y
|
||||
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
|
||||
guard let profileTabItem = _profileTabItem else { return }
|
||||
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
|
||||
|
||||
self.context.authenticationService.updateActiveUserAccountPublisher
|
||||
.sink { [weak self] in
|
||||
self?.updateUserAccount()
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
} else {
|
||||
self.avatarURLObserver = nil
|
||||
}
|
||||
guard let self,
|
||||
let authContext = self.authContext,
|
||||
let account = authContext.mastodonAuthenticationBox.authentication.account() else { return }
|
||||
|
||||
self.avatarURL = account.avatarImageURL()
|
||||
|
||||
// a11y
|
||||
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
|
||||
guard let profileTabItem = _profileTabItem else { return }
|
||||
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback)
|
||||
|
||||
self.context.authenticationService.updateActiveUserAccountPublisher
|
||||
.sink { [weak self] in
|
||||
self?.updateUserAccount()
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
|
||||
self.meProfileViewController.viewModel = ProfileViewModel(context: self.context, authContext: authContext, account: account, relationship: nil, me: account)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
@ -308,11 +225,11 @@ extension MainTabBarController {
|
||||
$currentTab
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] tab in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
self.updateAvatarButtonAppearance()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
updateTabBarDisplay()
|
||||
}
|
||||
|
||||
@ -366,7 +283,7 @@ extension MainTabBarController {
|
||||
case .search:
|
||||
assert(Thread.isMainThread)
|
||||
// double tapping search tab opens the search bar without additional taps
|
||||
searchViewController?.searchBar.becomeFirstResponder()
|
||||
searchViewController.searchBar.becomeFirstResponder()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -401,8 +318,7 @@ extension MainTabBarController {
|
||||
private func layoutAvatarButton() {
|
||||
guard avatarButton.superview == nil else { return }
|
||||
|
||||
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
|
||||
guard let profileTabItem = _profileTabItem else { return }
|
||||
guard let profileTabItem = meProfileViewController.tabBarItem else { return }
|
||||
guard let view = profileTabItem.value(forKey: "view") as? UIView else {
|
||||
return
|
||||
}
|
||||
@ -450,36 +366,12 @@ extension MainTabBarController {
|
||||
guard let authContext = authContext else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
let profileResponse = try await context.apiService.authenticatedUserInfo(
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
if let user = authContext.mastodonAuthenticationBox.authentication.user(
|
||||
in: context.managedObjectContext
|
||||
) {
|
||||
user.update(
|
||||
property: .init(
|
||||
entity: profileResponse.value,
|
||||
domain: authContext.mastodonAuthenticationBox.domain
|
||||
)
|
||||
)
|
||||
}
|
||||
let profileResponse = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
FileManager.default.store(account: profileResponse.value, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarController {
|
||||
|
||||
var notificationViewController: NotificationViewController? {
|
||||
return viewController(of: NotificationViewController.self)
|
||||
}
|
||||
|
||||
var searchViewController: SearchViewController? {
|
||||
return viewController(of: SearchViewController.self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITabBarControllerDelegate
|
||||
extension MainTabBarController: UITabBarControllerDelegate {
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||
|
@ -127,8 +127,8 @@ extension RootSplitViewController {
|
||||
|
||||
// MARK: - ContentSplitViewControllerDelegate
|
||||
extension RootSplitViewController: ContentSplitViewControllerDelegate {
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
|
||||
guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: Tab) {
|
||||
guard let _ = Tab.allCases.firstIndex(of: tab) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
@ -158,8 +158,8 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: MainTabBarController.Tab) {
|
||||
guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
|
||||
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: Tab) {
|
||||
guard let _ = Tab.allCases.firstIndex(of: tab) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
@ -170,7 +170,7 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate {
|
||||
guard !isPrimaryDisplay else {
|
||||
return
|
||||
}
|
||||
contentSplitViewController.mainTabBarController.searchViewController?.searchBar.becomeFirstResponder()
|
||||
contentSplitViewController.mainTabBarController.searchViewController.searchBar.becomeFirstResponder()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
protocol SidebarViewControllerDelegate: AnyObject {
|
||||
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
|
||||
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: Tab)
|
||||
func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView)
|
||||
func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ final class SidebarViewModel {
|
||||
let authContext: AuthContext?
|
||||
@Published private var isSidebarDataSourceReady = false
|
||||
@Published private var isAvatarButtonDataReady = false
|
||||
@Published var currentTab: MainTabBarController.Tab = .home
|
||||
@Published var currentTab: Tab = .home
|
||||
|
||||
// output
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
|
||||
@ -57,7 +57,7 @@ extension SidebarViewModel {
|
||||
}
|
||||
|
||||
enum Item: Hashable {
|
||||
case tab(MainTabBarController.Tab)
|
||||
case tab(Tab)
|
||||
case setting
|
||||
case compose
|
||||
}
|
||||
@ -69,18 +69,19 @@ extension SidebarViewModel {
|
||||
collectionView: UICollectionView,
|
||||
secondaryCollectionView: UICollectionView
|
||||
) {
|
||||
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in
|
||||
guard let self = self else { return }
|
||||
|
||||
let imageURL: URL? = {
|
||||
switch item {
|
||||
case .me:
|
||||
let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext)
|
||||
return user?.avatarImageURL()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, Tab> { [weak self] cell, indexPath, item in
|
||||
guard let self else { return }
|
||||
|
||||
let imageURL: URL?
|
||||
switch item {
|
||||
case .me:
|
||||
let account = self.authContext?.mastodonAuthenticationBox.authentication.account()
|
||||
imageURL = account?.avatarImageURL()
|
||||
case .home, .search, .compose, .notifications:
|
||||
// no custom avatar for other tabs
|
||||
imageURL = nil
|
||||
}
|
||||
|
||||
cell.item = SidebarListContentView.Item(
|
||||
isActive: false,
|
||||
accessoryImage: item == .me ? self.chevronImage : nil,
|
||||
@ -104,39 +105,40 @@ extension SidebarViewModel {
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
switch item {
|
||||
case .notifications:
|
||||
Publishers.CombineLatest(
|
||||
self.context.notificationService.unreadNotificationCountDidUpdate,
|
||||
self.$currentTab
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] authentication, currentTab in
|
||||
guard let cell = cell else { return }
|
||||
|
||||
let hasUnreadPushNotification: Bool = {
|
||||
guard let accessToken = self.authContext?.mastodonAuthenticationBox.userAuthorization.accessToken else { return false }
|
||||
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
|
||||
return count > 0
|
||||
}()
|
||||
|
||||
let image: UIImage
|
||||
if hasUnreadPushNotification {
|
||||
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
|
||||
image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)!
|
||||
} else {
|
||||
image = MainTabBarController.Tab.notifications.image
|
||||
case .notifications:
|
||||
Publishers.CombineLatest(
|
||||
self.context.notificationService.unreadNotificationCountDidUpdate,
|
||||
self.$currentTab
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] authentication, currentTab in
|
||||
guard let cell = cell else { return }
|
||||
|
||||
let hasUnreadPushNotification: Bool = {
|
||||
guard let accessToken = self.authContext?.mastodonAuthenticationBox.userAuthorization.accessToken else { return false }
|
||||
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
|
||||
return count > 0
|
||||
}()
|
||||
|
||||
let image: UIImage
|
||||
if hasUnreadPushNotification {
|
||||
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
|
||||
image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)!
|
||||
} else {
|
||||
image = Tab.notifications.image
|
||||
}
|
||||
cell.item?.image = image
|
||||
cell.item?.activeImage = image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)
|
||||
cell.setNeedsUpdateConfiguration()
|
||||
}
|
||||
cell.item?.image = image
|
||||
cell.item?.activeImage = image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)
|
||||
cell.setNeedsUpdateConfiguration()
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
case .me:
|
||||
guard let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
|
||||
let currentUserDisplayName = user.displayNameWithFallback
|
||||
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
|
||||
default:
|
||||
break
|
||||
.store(in: &cell.disposeBag)
|
||||
case .me:
|
||||
guard let account = self.authContext?.mastodonAuthenticationBox.authentication.account() else { return }
|
||||
|
||||
let currentUserDisplayName = account.displayNameWithFallback
|
||||
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
|
||||
case .compose, .home, .search:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
71
Mastodon/Scene/Root/Tab.swift
Normal file
71
Mastodon/Scene/Root/Tab.swift
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
|
||||
enum Tab: Int, CaseIterable {
|
||||
case home
|
||||
case search
|
||||
case compose
|
||||
case notifications
|
||||
case me
|
||||
|
||||
var tag: Int {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home: return L10n.Common.Controls.Tabs.home
|
||||
case .search: return L10n.Common.Controls.Tabs.searchAndExplore
|
||||
case .compose: return L10n.Common.Controls.Actions.compose
|
||||
case .notifications: return L10n.Common.Controls.Tabs.notifications
|
||||
case .me: return L10n.Common.Controls.Tabs.profile
|
||||
}
|
||||
}
|
||||
|
||||
var inputLabels: [String]? {
|
||||
switch self {
|
||||
case .home, .compose, .notifications, .me:
|
||||
return nil
|
||||
case .search:
|
||||
return [
|
||||
L10n.Common.Controls.Tabs.A11Y.search,
|
||||
L10n.Common.Controls.Tabs.A11Y.explore,
|
||||
L10n.Common.Controls.Tabs.searchAndExplore
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .home: return UIImage(systemName: "house")!
|
||||
case .search: return UIImage(systemName: "magnifyingglass")!
|
||||
case .compose: return UIImage(systemName: "square.and.pencil")!
|
||||
case .notifications: return UIImage(systemName: "bell")!
|
||||
case .me: return UIImage(systemName: "person")!
|
||||
}
|
||||
}
|
||||
|
||||
var selectedImage: UIImage {
|
||||
return image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
|
||||
var largeImage: UIImage {
|
||||
return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
func configureTabBarItem(with tab: Tab) {
|
||||
title = tab.title
|
||||
tabBarItem.tag = tab.tag
|
||||
tabBarItem.title = tab.title // needs for acessiblity large content label
|
||||
tabBarItem.image = tab.image.imageWithoutBaseline()
|
||||
tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
|
||||
tabBarItem.accessibilityLabel = tab.title
|
||||
tabBarItem.accessibilityUserInputLabels = tab.inputLabels
|
||||
tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||
var searchTransitionController = SearchTransitionController()
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: SearchViewModel!
|
||||
var viewModel: SearchViewModel?
|
||||
|
||||
// use AutoLayout could set search bar margin automatically to
|
||||
// layout alongside with split mode button (on iPad)
|
||||
@ -37,7 +37,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||
let searchBarTapPublisher = PassthroughSubject<String, Never>()
|
||||
|
||||
private(set) lazy var discoveryViewController: DiscoveryViewController? = {
|
||||
guard let authContext = viewModel.authContext else { return nil }
|
||||
guard let authContext = viewModel?.authContext else { return nil }
|
||||
let viewController = DiscoveryViewController()
|
||||
viewController.context = context
|
||||
viewController.coordinator = coordinator
|
||||
@ -70,7 +70,7 @@ extension SearchViewController {
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.viewDidAppeared.send()
|
||||
viewModel?.viewDidAppeared.send()
|
||||
|
||||
// note:
|
||||
// need set alpha because (maybe) SDK forget set alpha back
|
||||
@ -110,7 +110,7 @@ extension SearchViewController {
|
||||
.sink { [weak self] initialText in
|
||||
guard let self = self else { return }
|
||||
// push to search detail
|
||||
guard let authContext = self.viewModel.authContext else { return }
|
||||
guard let authContext = self.viewModel?.authContext else { return }
|
||||
let searchDetailViewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: initialText)
|
||||
searchDetailViewModel.needsBecomeFirstResponder = true
|
||||
self.navigationController?.delegate = self.searchTransitionController
|
||||
|
@ -69,7 +69,6 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
|
||||
)
|
||||
|
||||
let authContext = self.authContext
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
|
||||
Task {
|
||||
let searchResult = try await context.apiService.search(
|
||||
|
@ -50,8 +50,7 @@ extension SearchHistoryViewController {
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? []
|
||||
viewModel.items = (try? FileManager.default.searchItems(for: authContext.mastodonAuthenticationBox)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,9 +102,7 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa
|
||||
_ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView,
|
||||
clearButtonDidPressed button: UIButton
|
||||
) {
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
|
||||
FileManager.default.removeSearchHistory(forUser: userID)
|
||||
FileManager.default.removeSearchHistory(for: authContext.mastodonAuthenticationBox)
|
||||
viewModel.items = []
|
||||
}
|
||||
}
|
||||
@ -113,7 +110,6 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa
|
||||
//MARK: - SearchResultOverviewCoordinatorDelegate
|
||||
extension SearchHistoryViewController: SearchResultOverviewCoordinatorDelegate {
|
||||
func newSearchHistoryItemAdded(_ coordinator: SearchResultOverviewCoordinator) {
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? []
|
||||
viewModel.items = (try? FileManager.default.searchItems(for: authContext.mastodonAuthenticationBox)) ?? []
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ final class SearchHistoryViewModel {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.items = (try? FileManager.default.searchItems(forUser: authContext.mastodonAuthenticationBox.userID)) ?? []
|
||||
self.items = (try? FileManager.default.searchItems(for: authContext.mastodonAuthenticationBox)) ?? []
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ extension SearchResultSection {
|
||||
case .account(let account, let relationship):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
|
||||
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell }
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return cell }
|
||||
|
||||
cell.userView.setButtonState(.loading)
|
||||
cell.configure(
|
||||
@ -110,21 +110,4 @@ extension SearchResultSection {
|
||||
delegate: configuration.statusViewTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
static func configure(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
tableView: UITableView,
|
||||
cell: UserTableViewCell,
|
||||
viewModel: UserTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
cell.configure(
|
||||
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.userTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -65,11 +65,6 @@ extension SearchResultViewController {
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
case .user(let user):
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
user: user
|
||||
)
|
||||
case .hashtag(let tag):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
|
@ -152,6 +152,7 @@ extension SearchResultViewModel.State {
|
||||
await viewModel.dataController.reset()
|
||||
viewModel.hashtags = []
|
||||
}
|
||||
|
||||
|
||||
// due to combine relationships must be set first
|
||||
var existingRelationships = viewModel.relationships
|
||||
@ -159,7 +160,7 @@ extension SearchResultViewModel.State {
|
||||
existingRelationships.append(hashtag)
|
||||
}
|
||||
viewModel.relationships = existingRelationships
|
||||
|
||||
|
||||
await viewModel.dataController.appendRecords(statuses)
|
||||
|
||||
var existingHashtags = viewModel.hashtags
|
||||
|
@ -2,13 +2,18 @@
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
protocol AboutInstanceViewControllerDelegate: AnyObject {
|
||||
func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account)
|
||||
func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String)
|
||||
}
|
||||
|
||||
class AboutInstanceViewController: UIViewController {
|
||||
class AboutInstanceViewController: UIViewController, NeedsDependency, AuthContextProvider {
|
||||
|
||||
var authContext: AuthContext
|
||||
var context: AppContext!
|
||||
var coordinator: SceneCoordinator!
|
||||
|
||||
weak var delegate: AboutInstanceViewControllerDelegate?
|
||||
var dataSource: AboutInstanceTableViewDataSource?
|
||||
@ -19,7 +24,12 @@ class AboutInstanceViewController: UIViewController {
|
||||
|
||||
var instance: Mastodon.Entity.V2.Instance?
|
||||
|
||||
init() {
|
||||
init(context: AppContext, authContext: AuthContext, coordinator: SceneCoordinator) {
|
||||
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.coordinator = coordinator
|
||||
|
||||
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.register(ContactAdminTableViewCell.self, forCellReuseIdentifier: ContactAdminTableViewCell.reuseIdentifier)
|
||||
|
@ -4,6 +4,7 @@ import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
|
||||
enum ServerDetailsTab: Int, CaseIterable {
|
||||
case about = 0
|
||||
@ -36,7 +37,7 @@ class ServerDetailsViewController: UIViewController {
|
||||
let instanceRulesViewController: InstanceRulesViewController
|
||||
let containerView: UIView
|
||||
|
||||
init(domain: String) {
|
||||
init(domain: String, appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) {
|
||||
segmentedControl = UISegmentedControl()
|
||||
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@ -47,7 +48,7 @@ class ServerDetailsViewController: UIViewController {
|
||||
containerView = UIView()
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
aboutInstanceViewController = AboutInstanceViewController()
|
||||
aboutInstanceViewController = AboutInstanceViewController(context: appContext, authContext: authContext, coordinator: sceneCoordinator)
|
||||
instanceRulesViewController = InstanceRulesViewController()
|
||||
|
||||
pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
|
||||
|
@ -70,7 +70,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate {
|
||||
|
||||
navigationController.pushViewController(notificationViewController, animated: true)
|
||||
case .serverDetails(let domain):
|
||||
let serverDetailsViewController = ServerDetailsViewController(domain: domain)
|
||||
let serverDetailsViewController = ServerDetailsViewController(domain: domain, appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator)
|
||||
serverDetailsViewController.delegate = self
|
||||
|
||||
appContext.apiService.instanceV2(domain: domain)
|
||||
@ -216,15 +216,10 @@ extension SettingsCoordinator: ServerDetailsViewControllerDelegate {
|
||||
}
|
||||
|
||||
extension SettingsCoordinator: AboutInstanceViewControllerDelegate {
|
||||
@MainActor func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) {
|
||||
@MainActor
|
||||
func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) {
|
||||
Task {
|
||||
let user = try await appContext.apiService.fetchUser(username: account.username, domain: authContext.mastodonAuthenticationBox.domain, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
|
||||
let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, optionalMastodonUser: user)
|
||||
|
||||
_ = await MainActor.run {
|
||||
sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show)
|
||||
}
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: viewController, account: account)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,226 +0,0 @@
|
||||
//
|
||||
// NotificationView+Configuration.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import Meta
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import class CoreDataStack.Notification
|
||||
import MastodonSDK
|
||||
|
||||
extension NotificationView {
|
||||
public func configure(feed: MastodonFeed) {
|
||||
guard
|
||||
let notification = feed.notification,
|
||||
let managedObjectContext = viewModel.context?.managedObjectContext
|
||||
else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
MastodonNotification.fromEntity(
|
||||
notification,
|
||||
using: managedObjectContext,
|
||||
domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? ""
|
||||
).map(configure(notification:))
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationView {
|
||||
public func configure(notification: MastodonNotification) {
|
||||
viewModel.objects.insert(notification)
|
||||
|
||||
configureAuthor(notification: notification)
|
||||
|
||||
switch notification.entity.type {
|
||||
case .follow:
|
||||
setAuthorContainerBottomPaddingViewDisplay()
|
||||
case .followRequest:
|
||||
setFollowRequestAdaptiveMarginContainerViewDisplay()
|
||||
case .mention, .status:
|
||||
if let status = notification.status {
|
||||
statusView.configure(status: status)
|
||||
setStatusViewDisplay()
|
||||
}
|
||||
case .reblog, .favourite, .poll:
|
||||
if let status = notification.status {
|
||||
quoteStatusView.configure(status: status)
|
||||
setQuoteStatusViewDisplay()
|
||||
}
|
||||
case ._other:
|
||||
setAuthorContainerBottomPaddingViewDisplay()
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationView {
|
||||
private func configureAuthor(notification: MastodonNotification) {
|
||||
let author = notification.account
|
||||
// author avatar
|
||||
|
||||
Publishers.CombineLatest(
|
||||
author.publisher(for: \.avatar),
|
||||
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
|
||||
)
|
||||
.map { _ in author.avatarImageURL() }
|
||||
.assign(to: \.authorAvatarImageURL, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// author name
|
||||
Publishers.CombineLatest(
|
||||
author.publisher(for: \.displayName),
|
||||
author.publisher(for: \.emojis)
|
||||
)
|
||||
.map { _, emojis in
|
||||
do {
|
||||
let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis.asDictionary)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
return metaContent
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return PlaintextMetaContent(string: author.displayNameWithFallback)
|
||||
}
|
||||
}
|
||||
.assign(to: \.authorName, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// author username
|
||||
author.publisher(for: \.acct)
|
||||
.map { $0 as String? }
|
||||
.assign(to: \.authorUsername, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// timestamp
|
||||
viewModel.timestamp = notification.entity.createdAt
|
||||
|
||||
viewModel.visibility = notification.entity.status?.mastodonVisibility ?? ._other("")
|
||||
|
||||
// notification type indicator
|
||||
Publishers.CombineLatest(
|
||||
author.publisher(for: \.displayName),
|
||||
author.publisher(for: \.emojis)
|
||||
)
|
||||
.sink { [weak self] _, emojis in
|
||||
guard let self = self else { return }
|
||||
guard let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else {
|
||||
self.viewModel.notificationIndicatorText = nil
|
||||
return
|
||||
}
|
||||
self.viewModel.type = type
|
||||
|
||||
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
|
||||
let content = MastodonContent(content: text, emojis: emojis)
|
||||
guard let metaContent = try? MastodonMetaContent.convert(document: content) else {
|
||||
return PlaintextMetaContent(string: text)
|
||||
}
|
||||
return metaContent
|
||||
}
|
||||
|
||||
// TODO: fix the i18n. The subject should assert place at the string beginning
|
||||
switch type {
|
||||
case .follow:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.followedYou,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case .followRequest:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case .mention:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.mentionedYou,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case .reblog:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case .favourite:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case .poll:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: L10n.Scene.Notification.NotificationDescription.pollHasEnded,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case .status:
|
||||
self.viewModel.notificationIndicatorText = createMetaContent(
|
||||
text: .empty,
|
||||
emojis: emojis.asDictionary
|
||||
)
|
||||
case ._other:
|
||||
self.viewModel.notificationIndicatorText = nil
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
let authContext = viewModel.authContext
|
||||
// isMuting
|
||||
author.publisher(for: \.mutingBy)
|
||||
.map { mutingBy in
|
||||
guard let authContext = authContext else { return false }
|
||||
return mutingBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID
|
||||
&& $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isMuting, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isBlocking
|
||||
author.publisher(for: \.blockingBy)
|
||||
.map { blockingBy in
|
||||
guard let authContext = authContext else { return false }
|
||||
return blockingBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID
|
||||
&& $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isBlocking, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// isMyself
|
||||
Publishers.CombineLatest(
|
||||
author.publisher(for: \.domain),
|
||||
author.publisher(for: \.id)
|
||||
)
|
||||
.map { domain, id in
|
||||
guard let authContext = authContext else { return false }
|
||||
return authContext.mastodonAuthenticationBox.domain == domain
|
||||
&& authContext.mastodonAuthenticationBox.userID == id
|
||||
}
|
||||
.assign(to: \.isMyself, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// follow request state
|
||||
viewModel.followRequestState = notification.followRequestState
|
||||
viewModel.transientFollowRequestState = notification.transientFollowRequestState
|
||||
|
||||
// Following
|
||||
author.publisher(for: \.followingBy)
|
||||
.map { [weak viewModel] followingBy in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
return followingBy.contains(where: {
|
||||
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
|
||||
})
|
||||
}
|
||||
.assign(to: \.isFollowed, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
}
|
@ -17,68 +17,16 @@ import MastodonSDK
|
||||
import MastodonAsset
|
||||
|
||||
extension UserView {
|
||||
public func configure(user: MastodonUser, delegate: UserViewDelegate?) {
|
||||
self.delegate = delegate
|
||||
viewModel.user = user
|
||||
viewModel.account = nil
|
||||
viewModel.relationship = nil
|
||||
|
||||
Publishers.CombineLatest(
|
||||
user.publisher(for: \.avatar),
|
||||
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
|
||||
)
|
||||
.map { _ in user.avatarImageURL() }
|
||||
.assign(to: \.authorAvatarImageURL, 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)
|
||||
|
||||
user.publisher(for: \.followersCount)
|
||||
.map { Int($0) }
|
||||
.assign(to: \.authorFollowers, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
user.publisher(for: \.fields)
|
||||
.map { fields in
|
||||
let firstVerified = fields.first(where: { $0.verifiedAt != nil })
|
||||
return firstVerified?.value
|
||||
}
|
||||
.assign(to: \.authorVerifiedLink, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func configure(with account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserViewDelegate?) {
|
||||
viewModel.account = account
|
||||
viewModel.relationship = relationship
|
||||
viewModel.user = nil
|
||||
self.delegate = delegate
|
||||
|
||||
let authorUsername = PlaintextMetaContent(string: "@\(account.username)")
|
||||
authorUsernameLabel.configure(content: authorUsername)
|
||||
|
||||
do {
|
||||
let emojis = account.emojis?.asDictionary ?? [:]
|
||||
let emojis = account.emojis.asDictionary
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: emojis)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
authorNameLabel.configure(content: metaContent)
|
||||
|
@ -14,14 +14,14 @@ import MastodonSDK
|
||||
|
||||
extension UserTableViewCell {
|
||||
final class ViewModel {
|
||||
let user: MastodonUser
|
||||
|
||||
let account: Mastodon.Entity.Account
|
||||
|
||||
let followedUsers: AnyPublisher<[String], Never>
|
||||
let blockedUsers: AnyPublisher<[String], Never>
|
||||
let followRequestedUsers: AnyPublisher<[String], Never>
|
||||
|
||||
init(user: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
|
||||
self.user = user
|
||||
init(account: Mastodon.Entity.Account, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) {
|
||||
self.account = account
|
||||
self.followedUsers = followedUsers
|
||||
self.followRequestedUsers = followRequestedUsers
|
||||
self.blockedUsers = blockedUsers
|
||||
@ -32,7 +32,7 @@ extension UserTableViewCell {
|
||||
extension UserTableViewCell {
|
||||
|
||||
func configure(
|
||||
me: MastodonUser,
|
||||
me: Mastodon.Entity.Account,
|
||||
tableView: UITableView,
|
||||
account: Mastodon.Entity.Account,
|
||||
relationship: Mastodon.Entity.Relationship?,
|
||||
@ -45,69 +45,16 @@ extension UserTableViewCell {
|
||||
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
func configure(
|
||||
me: MastodonUser? = nil,
|
||||
tableView: UITableView,
|
||||
viewModel: ViewModel,
|
||||
delegate: UserTableViewCellDelegate?
|
||||
) {
|
||||
userView.configure(user: viewModel.user, delegate: delegate)
|
||||
|
||||
guard let me = me else {
|
||||
return userView.setButtonState(.none)
|
||||
}
|
||||
|
||||
if viewModel.user == me {
|
||||
userView.setButtonState(.none)
|
||||
} else {
|
||||
userView.setButtonState(.loading)
|
||||
}
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
viewModel.followedUsers,
|
||||
viewModel.followRequestedUsers,
|
||||
viewModel.blockedUsers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] followed, requested, blocked in
|
||||
if viewModel.user == me {
|
||||
self?.userView.setButtonState(.none)
|
||||
} else if blocked.contains(viewModel.user.id) {
|
||||
self?.userView.setButtonState(.blocked)
|
||||
} else if followed.contains(viewModel.user.id) {
|
||||
self?.userView.setButtonState(.unfollow)
|
||||
} else if requested.contains(viewModel.user.id) {
|
||||
self?.userView.setButtonState(.pending)
|
||||
} else if viewModel.user.locked {
|
||||
self?.userView.setButtonState(.request)
|
||||
} else if viewModel.user != me {
|
||||
self?.userView.setButtonState(.follow)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
self.delegate = delegate
|
||||
}
|
||||
}
|
||||
|
||||
extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider {
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserViewButtonAction(
|
||||
dependency: self,
|
||||
user: user.asRecord,
|
||||
buttonState: state
|
||||
)
|
||||
}
|
||||
}
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: MastodonUser?) {
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: Mastodon.Entity.Account?) {
|
||||
Task {
|
||||
await MainActor.run { view.setButtonState(.loading) }
|
||||
|
||||
try await DataSourceFacade.responseToUserViewButtonAction(
|
||||
dependency: self,
|
||||
user: account,
|
||||
account: account,
|
||||
buttonState: state
|
||||
)
|
||||
|
||||
@ -128,7 +75,6 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro
|
||||
view.viewModel.relationship = relationship
|
||||
view.updateButtonState(with: relationship, isMe: isMe)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,9 @@ extension SuggestionAccountViewController: UITableViewDelegate {
|
||||
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
switch item {
|
||||
case .account(let account, _):
|
||||
Task { await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) }
|
||||
Task {
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
}
|
||||
}
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
@ -121,7 +121,7 @@ final class SuggestionAccountViewModel: NSObject {
|
||||
taskGroup.addTask {
|
||||
try? await DataSourceFacade.responseToUserViewButtonAction(
|
||||
dependency: dependency,
|
||||
user: account,
|
||||
account: account,
|
||||
buttonState: .follow
|
||||
)
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
@ -91,7 +91,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
|
||||
|
||||
let metaContent: MetaContent = {
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: account.note, emojis: account.emojis?.asDictionary ?? [:])
|
||||
let mastodonContent = MastodonContent(content: account.note, emojis: account.emojis.asDictionary)
|
||||
return try MastodonMetaContent.convert(document: mastodonContent)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
|
@ -24,7 +24,6 @@ final class RemoteThreadViewModel: ThreadViewModel {
|
||||
)
|
||||
|
||||
Task { @MainActor in
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
let response = try await context.apiService.status(
|
||||
statusID: statusID,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
@ -48,7 +47,6 @@ final class RemoteThreadViewModel: ThreadViewModel {
|
||||
)
|
||||
|
||||
Task { @MainActor in
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
let response = try await context.apiService.notification(
|
||||
notificationID: notificationID,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
|
@ -75,7 +75,7 @@ class ThreadViewModel {
|
||||
// bind titleView
|
||||
self.navigationBarTitle = {
|
||||
let title = L10n.Scene.Thread.title(status.entity.account.displayNameWithFallback)
|
||||
let content = MastodonContent(content: title, emojis: status.entity.account.emojis?.asDictionary ?? [:])
|
||||
let content = MastodonContent(content: title, emojis: status.entity.account.emojis.asDictionary)
|
||||
return try? MastodonMetaContent.convert(document: content)
|
||||
}()
|
||||
}
|
||||
|
@ -92,9 +92,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
// trigger authenticated user account update
|
||||
AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send()
|
||||
|
||||
// update mutes and blocks and remove related data
|
||||
AppContext.shared.instanceService.updateMutesAndBlocks()
|
||||
|
||||
if let shortcutItem = savedShortCutItem {
|
||||
Task {
|
||||
@ -137,16 +134,34 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
switch (profile, statusID) {
|
||||
case (profile, nil):
|
||||
let profileViewModel = RemoteProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authContext: authContext,
|
||||
acct: incomingURL.absoluteString
|
||||
)
|
||||
self.coordinator?.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: nil,
|
||||
transition: .show
|
||||
)
|
||||
Task {
|
||||
let authenticationBox = authContext.mastodonAuthenticationBox
|
||||
|
||||
guard let me = authenticationBox.authentication.account() else { return }
|
||||
|
||||
guard let account = try await AppContext.shared.apiService.search(
|
||||
query: .init(q: incomingURL.absoluteString, type: .accounts, resolve: true),
|
||||
authenticationBox: authenticationBox
|
||||
).value.accounts.first else { return }
|
||||
|
||||
guard let relationship = try await AppContext.shared.apiService.relationship(
|
||||
forAccounts: [account],
|
||||
authenticationBox: authenticationBox
|
||||
).value.first else { return }
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authContext: authContext,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
_ = self.coordinator?.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: nil,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
case (profile, statusID):
|
||||
Task {
|
||||
@ -248,10 +263,10 @@ extension SceneDelegate {
|
||||
|
||||
if !UIApplication.shared.canOpenURL(url) { return }
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
print("source application = \(sendingAppID ?? "Unknown")")
|
||||
print("url = \(url)")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
switch url.host {
|
||||
case "post":
|
||||
@ -264,16 +279,39 @@ extension SceneDelegate {
|
||||
let authContext = coordinator?.authContext
|
||||
else { return }
|
||||
|
||||
let profileViewModel = RemoteProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authContext: authContext,
|
||||
acct: components[1]
|
||||
)
|
||||
self.coordinator?.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: nil,
|
||||
transition: .show
|
||||
)
|
||||
Task {
|
||||
do {
|
||||
let authenticationBox = authContext.mastodonAuthenticationBox
|
||||
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return }
|
||||
|
||||
guard let account = try await AppContext.shared.apiService.search(
|
||||
query: .init(q: components[1], type: .accounts, resolve: true),
|
||||
authenticationBox: authenticationBox
|
||||
).value.accounts.first else { return }
|
||||
|
||||
guard let relationship = try await AppContext.shared.apiService.relationship(
|
||||
forAccounts: [account],
|
||||
authenticationBox: authenticationBox
|
||||
).value.first else { return }
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authContext: authContext,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
|
||||
self.coordinator?.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: nil,
|
||||
transition: .show
|
||||
)
|
||||
} catch {
|
||||
// fail silently
|
||||
}
|
||||
}
|
||||
case "status":
|
||||
let components = url.pathComponents
|
||||
guard
|
||||
|
@ -59,7 +59,7 @@ extension SendPostIntentHandler: SendPostIntentHandling {
|
||||
}
|
||||
mastodonAuthentications = [authentication]
|
||||
} else {
|
||||
mastodonAuthentications = try accounts.mastodonAuthentication(in: managedObjectContext)
|
||||
mastodonAuthentications = try accounts.mastodonAuthentication()
|
||||
}
|
||||
|
||||
let authenticationBoxes = mastodonAuthentications.map { authentication in
|
||||
@ -149,7 +149,7 @@ extension SendPostIntentHandler: SendPostIntentHandling {
|
||||
}
|
||||
|
||||
func provideAccountsOptionsCollection(for intent: SendPostIntent) async throws -> INObjectCollection<Account> {
|
||||
let accounts = try await Account.fetch(in: managedObjectContext)
|
||||
let accounts = try await Account.fetch()
|
||||
return .init(items: accounts)
|
||||
}
|
||||
|
||||
|
@ -14,34 +14,29 @@ import MastodonCore
|
||||
extension Account {
|
||||
|
||||
@MainActor
|
||||
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
|
||||
// get accounts
|
||||
let accounts: [Account] = try await managedObjectContext.perform {
|
||||
let results = AuthenticationServiceProvider.shared.authentications
|
||||
let accounts = results.compactMap { mastodonAuthentication -> Account? in
|
||||
guard let user = mastodonAuthentication.user(in: managedObjectContext) else {
|
||||
return nil
|
||||
}
|
||||
let account = Account(
|
||||
identifier: mastodonAuthentication.identifier.uuidString,
|
||||
display: user.displayNameWithFallback,
|
||||
subtitle: user.acctWithDomain,
|
||||
image: user.avatarImageURL().flatMap { INImage(url: $0) }
|
||||
)
|
||||
account.name = user.displayNameWithFallback
|
||||
account.username = user.acctWithDomain
|
||||
return account
|
||||
static func fetch() async throws -> [Account] {
|
||||
let accounts = AuthenticationServiceProvider.shared.authentications.compactMap { mastodonAuthentication -> Account? in
|
||||
guard let authenticatedAccount = mastodonAuthentication.account() else {
|
||||
return nil
|
||||
}
|
||||
return accounts
|
||||
} // end managedObjectContext.perform
|
||||
let account = Account(
|
||||
identifier: mastodonAuthentication.identifier.uuidString,
|
||||
display: authenticatedAccount.displayNameWithFallback,
|
||||
subtitle: authenticatedAccount.acctWithDomain,
|
||||
image: authenticatedAccount.avatarImageURL().flatMap { INImage(url: $0) }
|
||||
)
|
||||
account.name = authenticatedAccount.displayNameWithFallback
|
||||
account.username = authenticatedAccount.acctWithDomain
|
||||
return account
|
||||
}
|
||||
|
||||
return accounts
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension Array where Element == Account {
|
||||
func mastodonAuthentication(in managedObjectContext: NSManagedObjectContext) throws -> [MastodonAuthentication] {
|
||||
func mastodonAuthentication() throws -> [MastodonAuthentication] {
|
||||
let identifiers = self
|
||||
.compactMap { $0.identifier }
|
||||
.compactMap { UUID(uuidString: $0) }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user