forked from zelo72/mastodon-ios
feat: add For You tab for Discovery scene
This commit is contained in:
parent
8e0d526708
commit
b0fca49413
|
@ -214,7 +214,6 @@
|
||||||
DB336F3F278E668C0031E64B /* StatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */; };
|
DB336F3F278E668C0031E64B /* StatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */; };
|
||||||
DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F40278E68480031E64B /* StatusView+Configuration.swift */; };
|
DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F40278E68480031E64B /* StatusView+Configuration.swift */; };
|
||||||
DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F42278EB1680031E64B /* MediaView+Configuration.swift */; };
|
DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F42278EB1680031E64B /* MediaView+Configuration.swift */; };
|
||||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
|
||||||
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; };
|
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; };
|
||||||
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; };
|
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; };
|
||||||
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; };
|
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; };
|
||||||
|
@ -232,6 +231,9 @@
|
||||||
DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */; };
|
DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */; };
|
||||||
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */; };
|
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */; };
|
||||||
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */; };
|
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */; };
|
||||||
|
DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */; };
|
||||||
|
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */; };
|
||||||
|
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */; };
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
|
||||||
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
|
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
|
||||||
|
@ -370,7 +372,6 @@
|
||||||
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; };
|
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; };
|
||||||
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; };
|
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; };
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; };
|
|
||||||
DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; };
|
DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; };
|
||||||
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; };
|
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; };
|
||||||
DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; };
|
DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; };
|
||||||
|
@ -502,8 +503,6 @@
|
||||||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; };
|
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; };
|
||||||
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; };
|
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; };
|
||||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
|
||||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */; };
|
|
||||||
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
|
|
||||||
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
|
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
|
||||||
DBB8AB4826AED09C00F6D281 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DBB8AB4726AED09C00F6D281 /* MastodonSDK */; };
|
DBB8AB4826AED09C00F6D281 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DBB8AB4726AED09C00F6D281 /* MastodonSDK */; };
|
||||||
DBB8AB4A26AED0B500F6D281 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4926AED0B500F6D281 /* APIService.swift */; };
|
DBB8AB4A26AED0B500F6D281 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4926AED0B500F6D281 /* APIService.swift */; };
|
||||||
|
@ -518,7 +517,6 @@
|
||||||
DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; };
|
DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; };
|
||||||
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; };
|
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; };
|
||||||
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
|
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
|
||||||
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; };
|
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; };
|
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; };
|
||||||
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; };
|
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; };
|
||||||
|
@ -594,7 +592,6 @@
|
||||||
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; };
|
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; };
|
||||||
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; };
|
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; };
|
||||||
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; };
|
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; };
|
||||||
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; };
|
|
||||||
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; };
|
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; };
|
||||||
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
||||||
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07226A6913D006D7ED1 /* APIService.swift */; };
|
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07226A6913D006D7ED1 /* APIService.swift */; };
|
||||||
|
@ -945,7 +942,6 @@
|
||||||
DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||||
DB336F40278E68480031E64B /* StatusView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusView+Configuration.swift"; sourceTree = "<group>"; };
|
DB336F40278E68480031E64B /* StatusView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusView+Configuration.swift"; sourceTree = "<group>"; };
|
||||||
DB336F42278EB1680031E64B /* MediaView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaView+Configuration.swift"; sourceTree = "<group>"; };
|
DB336F42278EB1680031E64B /* MediaView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaView+Configuration.swift"; sourceTree = "<group>"; };
|
||||||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRelationshipActionButton.swift; sourceTree = "<group>"; };
|
|
||||||
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = "<group>"; };
|
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = "<group>"; };
|
||||||
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = "<group>"; };
|
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = "<group>"; };
|
||||||
|
@ -963,6 +959,9 @@
|
||||||
DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryNewsViewModel.swift; sourceTree = "<group>"; };
|
DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryNewsViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryNewsViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryNewsViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryNewsViewModel+State.swift"; sourceTree = "<group>"; };
|
DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryNewsViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
|
DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryForYouViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryForYouViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryForYouViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1119,7 +1118,6 @@
|
||||||
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = "<group>"; };
|
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = "<group>"; };
|
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreference.swift; sourceTree = "<group>"; };
|
|
||||||
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = "<group>"; };
|
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = "<group>"; };
|
||||||
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
|
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
|
||||||
DB6D9F4826353FD6008423CD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
|
DB6D9F4826353FD6008423CD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1261,15 +1259,12 @@
|
||||||
DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewModel.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>"; };
|
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>"; };
|
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardView.swift; sourceTree = "<group>"; };
|
|
||||||
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = "<group>"; };
|
|
||||||
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
|
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
DBB8AB4926AED0B500F6D281 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
DBB8AB4926AED0B500F6D281 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
||||||
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
||||||
DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||||
DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = "<group>"; };
|
DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
|
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
|
||||||
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
|
||||||
DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = "<group>"; };
|
DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = "<group>"; };
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
|
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2147,6 +2142,16 @@
|
||||||
path = News;
|
path = News;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB3E6FF62807C40500B035AE /* ForYou */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */,
|
||||||
|
DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */,
|
||||||
|
DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */,
|
||||||
|
);
|
||||||
|
path = ForYou;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB427DC925BAA00100D1B89D = {
|
DB427DC925BAA00100D1B89D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2378,7 +2383,6 @@
|
||||||
children = (
|
children = (
|
||||||
DBA465942696E387002B41DB /* AppPreference.swift */,
|
DBA465942696E387002B41DB /* AppPreference.swift */,
|
||||||
DB647C5826F1EA2700F7F82C /* WizardPreference.swift */,
|
DB647C5826F1EA2700F7F82C /* WizardPreference.swift */,
|
||||||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */,
|
|
||||||
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */,
|
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */,
|
||||||
DB1D842F26566512000346B3 /* KeyboardPreference.swift */,
|
DB1D842F26566512000346B3 /* KeyboardPreference.swift */,
|
||||||
DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */,
|
DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */,
|
||||||
|
@ -3040,9 +3044,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
|
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
|
||||||
DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */,
|
|
||||||
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */,
|
|
||||||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */,
|
|
||||||
DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */,
|
DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
|
@ -3061,7 +3062,6 @@
|
||||||
children = (
|
children = (
|
||||||
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
|
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
|
||||||
DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */,
|
DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */,
|
||||||
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */,
|
|
||||||
DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
|
DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
|
||||||
);
|
);
|
||||||
path = Helper;
|
path = Helper;
|
||||||
|
@ -3113,6 +3113,7 @@
|
||||||
DBDFF19828055A0900557A48 /* Posts */,
|
DBDFF19828055A0900557A48 /* Posts */,
|
||||||
DB3E6FDE2806A41200B035AE /* Hashtags */,
|
DB3E6FDE2806A41200B035AE /* Hashtags */,
|
||||||
DB3E6FED2806D7FC00B035AE /* News */,
|
DB3E6FED2806D7FC00B035AE /* News */,
|
||||||
|
DB3E6FF62807C40500B035AE /* ForYou */,
|
||||||
DBDFF19928055A1400557A48 /* DiscoveryViewController.swift */,
|
DBDFF19928055A1400557A48 /* DiscoveryViewController.swift */,
|
||||||
DBDFF19B28055BD600557A48 /* DiscoveryViewModel.swift */,
|
DBDFF19B28055BD600557A48 /* DiscoveryViewModel.swift */,
|
||||||
);
|
);
|
||||||
|
@ -3947,7 +3948,6 @@
|
||||||
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
|
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
|
||||||
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
|
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
|
||||||
DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */,
|
DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */,
|
||||||
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */,
|
|
||||||
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */,
|
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */,
|
||||||
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */,
|
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */,
|
||||||
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
|
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
|
||||||
|
@ -3974,7 +3974,6 @@
|
||||||
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */,
|
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */,
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
||||||
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */,
|
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */,
|
||||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
|
||||||
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */,
|
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */,
|
||||||
DB025B93278D6501002F581E /* Persistence.swift in Sources */,
|
DB025B93278D6501002F581E /* Persistence.swift in Sources */,
|
||||||
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
||||||
|
@ -4066,11 +4065,11 @@
|
||||||
DB697DD4278F4927004EF2F7 /* StatusTableViewCellDelegate.swift in Sources */,
|
DB697DD4278F4927004EF2F7 /* StatusTableViewCellDelegate.swift in Sources */,
|
||||||
DB0FCB902796C5EB006C02E2 /* APIService+Trend.swift in Sources */,
|
DB0FCB902796C5EB006C02E2 /* APIService+Trend.swift in Sources */,
|
||||||
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
|
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
|
||||||
|
DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */,
|
||||||
DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */,
|
DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */,
|
||||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
DB0FCB842796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift in Sources */,
|
DB0FCB842796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift in Sources */,
|
||||||
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
|
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
|
||||||
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
|
|
||||||
DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */,
|
DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */,
|
||||||
DB63F75C279956D000455B82 /* Persistence+Tag.swift in Sources */,
|
DB63F75C279956D000455B82 /* Persistence+Tag.swift in Sources */,
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
||||||
|
@ -4112,7 +4111,6 @@
|
||||||
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
|
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
|
||||||
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */,
|
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */,
|
||||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
|
||||||
DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */,
|
DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */,
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||||
DB025B95278D6530002F581E /* Persistence+MastodonUser.swift in Sources */,
|
DB025B95278D6530002F581E /* Persistence+MastodonUser.swift in Sources */,
|
||||||
|
@ -4178,6 +4176,7 @@
|
||||||
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
|
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
|
||||||
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
|
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
|
||||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||||
|
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
|
||||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||||
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */,
|
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */,
|
||||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||||
|
@ -4311,6 +4310,7 @@
|
||||||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
||||||
|
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
||||||
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
||||||
|
@ -4327,7 +4327,6 @@
|
||||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
||||||
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */,
|
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */,
|
||||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
|
||||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||||
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */,
|
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */,
|
||||||
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */,
|
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */,
|
||||||
|
@ -4424,7 +4423,6 @@
|
||||||
DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */,
|
DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */,
|
||||||
DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */,
|
DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */,
|
||||||
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */,
|
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */,
|
||||||
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */,
|
|
||||||
DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */,
|
DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>23</integer>
|
<integer>22</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -124,7 +124,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>22</integer>
|
<integer>23</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
enum DiscoveryItem: Hashable {
|
enum DiscoveryItem: Hashable {
|
||||||
case hashtag(Mastodon.Entity.Tag)
|
case hashtag(Mastodon.Entity.Tag)
|
||||||
case link(Mastodon.Entity.Link)
|
case link(Mastodon.Entity.Link)
|
||||||
|
case user(ManagedObjectRecord<MastodonUser>)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,9 @@ extension DiscoverySection {
|
||||||
) -> UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem> {
|
) -> UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem> {
|
||||||
tableView.register(TrendTableViewCell.self, forCellReuseIdentifier: String(describing: TrendTableViewCell.self))
|
tableView.register(TrendTableViewCell.self, forCellReuseIdentifier: String(describing: TrendTableViewCell.self))
|
||||||
tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: String(describing: NewsTableViewCell.self))
|
tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: String(describing: NewsTableViewCell.self))
|
||||||
|
tableView.register(ProfileCardTableViewCell.self, forCellReuseIdentifier: String(describing: ProfileCardTableViewCell.self))
|
||||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
|
||||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
||||||
switch item {
|
switch item {
|
||||||
case .hashtag(let tag):
|
case .hashtag(let tag):
|
||||||
|
@ -41,6 +42,17 @@ extension DiscoverySection {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NewsTableViewCell.self), for: indexPath) as! NewsTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NewsTableViewCell.self), for: indexPath) as! NewsTableViewCell
|
||||||
cell.newsView.configure(link: link)
|
cell.newsView.configure(link: link)
|
||||||
return cell
|
return cell
|
||||||
|
case .user(let record):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ProfileCardTableViewCell.self), for: indexPath) as! ProfileCardTableViewCell
|
||||||
|
context.managedObjectContext.performAndWait {
|
||||||
|
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||||
|
cell.profileCardView.configure(user: user)
|
||||||
|
}
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.map { $0?.user }
|
||||||
|
.assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel)
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
cell.activityIndicatorView.startAnimating()
|
cell.activityIndicatorView.startAnimating()
|
||||||
|
|
|
@ -9,53 +9,6 @@ import Foundation
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
extension MastodonUser {
|
|
||||||
|
|
||||||
public var displayNameWithFallback: String {
|
|
||||||
return !displayName.isEmpty ? displayName : username
|
|
||||||
}
|
|
||||||
|
|
||||||
public var acctWithDomain: String {
|
|
||||||
if !acct.contains("@") {
|
|
||||||
// Safe concat due to username cannot contains "@"
|
|
||||||
return username + "@" + domain
|
|
||||||
} else {
|
|
||||||
return acct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var domainFromAcct: String {
|
|
||||||
if !acct.contains("@") {
|
|
||||||
return domain
|
|
||||||
} else {
|
|
||||||
let domain = acct.split(separator: "@").last
|
|
||||||
return String(domain!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MastodonUser {
|
|
||||||
|
|
||||||
public func headerImageURL() -> URL? {
|
|
||||||
return URL(string: header)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func headerImageURLWithFallback(domain: String) -> URL {
|
|
||||||
return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")!
|
|
||||||
}
|
|
||||||
|
|
||||||
public func avatarImageURL() -> URL? {
|
|
||||||
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
|
||||||
return URL(string: string)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func avatarImageURLWithFallback(domain: String) -> URL {
|
|
||||||
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MastodonUser {
|
extension MastodonUser {
|
||||||
|
|
||||||
public var profileURL: URL {
|
public var profileURL: URL {
|
||||||
|
|
|
@ -22,13 +22,3 @@ extension MastodonEmoji {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Collection where Element == MastodonEmoji {
|
|
||||||
public var asDictionary: MastodonContent.Emojis {
|
|
||||||
var dictionary: MastodonContent.Emojis = [:]
|
|
||||||
for emoji in self {
|
|
||||||
dictionary[emoji.code] = emoji.url
|
|
||||||
}
|
|
||||||
return dictionary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ final class DiscoveryViewModel {
|
||||||
let discoveryPostsViewController: DiscoveryPostsViewController
|
let discoveryPostsViewController: DiscoveryPostsViewController
|
||||||
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
|
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
|
||||||
let discoveryNewsViewController: DiscoveryNewsViewController
|
let discoveryNewsViewController: DiscoveryNewsViewController
|
||||||
|
let discoveryForYouViewController: DiscoveryForYouViewController
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let barItems: [TMBarItemable] = {
|
let barItems: [TMBarItemable] = {
|
||||||
|
@ -33,6 +34,7 @@ final class DiscoveryViewModel {
|
||||||
discoveryPostsViewController,
|
discoveryPostsViewController,
|
||||||
discoveryHashtagsViewController,
|
discoveryHashtagsViewController,
|
||||||
discoveryNewsViewController,
|
discoveryNewsViewController,
|
||||||
|
discoveryForYouViewController,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +63,12 @@ final class DiscoveryViewModel {
|
||||||
viewController.viewModel = DiscoveryNewsViewModel(context: context)
|
viewController.viewModel = DiscoveryNewsViewModel(context: context)
|
||||||
return viewController
|
return viewController
|
||||||
}()
|
}()
|
||||||
|
discoveryForYouViewController = {
|
||||||
|
let viewController = DiscoveryForYouViewController()
|
||||||
|
setupDependency(viewController)
|
||||||
|
viewController.viewModel = DiscoveryForYouViewModel(context: context)
|
||||||
|
return viewController
|
||||||
|
}()
|
||||||
// end init
|
// end init
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
//
|
||||||
|
// DiscoveryForYouViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
|
final class DiscoveryForYouViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "DiscoveryForYouViewController", category: "ViewController")
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: DiscoveryForYouViewModel!
|
||||||
|
|
||||||
|
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||||
|
|
||||||
|
lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView()
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 100
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiscoveryForYouViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||||
|
ThemeService.shared.currentTheme
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] theme in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.view.backgroundColor = theme.secondarySystemBackgroundColor
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.setupDiffableDataSource(
|
||||||
|
tableView: tableView
|
||||||
|
)
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(DiscoveryForYouViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
viewModel.$isFetching
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isFetching in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !isFetching {
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiscoveryForYouViewController {
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
Task {
|
||||||
|
try await viewModel.fetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension DiscoveryForYouViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)")
|
||||||
|
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
|
||||||
|
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||||
|
let profileViewModel = CachedProfileViewModel(
|
||||||
|
context: context,
|
||||||
|
mastodonUser: user
|
||||||
|
)
|
||||||
|
coordinator.present(
|
||||||
|
scene: .profile(viewModel: profileViewModel),
|
||||||
|
from: self,
|
||||||
|
transition: .show
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: ScrollViewContainer
|
||||||
|
extension DiscoveryForYouViewController: ScrollViewContainer {
|
||||||
|
var scrollView: UIScrollView? {
|
||||||
|
tableView
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
//
|
||||||
|
// DiscoveryForYouViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension DiscoveryForYouViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
tableView: UITableView
|
||||||
|
) {
|
||||||
|
diffableDataSource = DiscoverySection.diffableDataSource(
|
||||||
|
tableView: tableView,
|
||||||
|
context: context,
|
||||||
|
configuration: DiscoverySection.Configuration()
|
||||||
|
)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
try await fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
userFetchedResultsController.$records
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] records in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()
|
||||||
|
snapshot.appendSections([.forYou])
|
||||||
|
|
||||||
|
let items = records.map { DiscoveryItem.user($0) }
|
||||||
|
snapshot.appendItems(items, toSection: .forYou)
|
||||||
|
|
||||||
|
diffableDataSource.applySnapshot(snapshot, animated: false)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
//
|
||||||
|
// DiscoveryForYouViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class DiscoveryForYouViewModel {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let userFetchedResultsController: UserFetchedResultsController
|
||||||
|
@Published var isFetching = false
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
|
||||||
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
self.userFetchedResultsController = UserFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: nil,
|
||||||
|
additionalPredicate: nil
|
||||||
|
)
|
||||||
|
// end init
|
||||||
|
|
||||||
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.map { $0?.domain }
|
||||||
|
.assign(to: \.domain, on: userFetchedResultsController)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiscoveryForYouViewModel {
|
||||||
|
func fetch() async throws {
|
||||||
|
guard !isFetching else { return }
|
||||||
|
isFetching = true
|
||||||
|
defer { isFetching = false }
|
||||||
|
|
||||||
|
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await context.apiService.suggestionAccountV2(
|
||||||
|
query: nil,
|
||||||
|
authenticationBox: authenticationBox
|
||||||
|
)
|
||||||
|
let userIDs = response.value.map { $0.account.id }
|
||||||
|
userFetchedResultsController.userIDs = userIDs
|
||||||
|
} catch {
|
||||||
|
// fallback V1
|
||||||
|
let response2 = try await context.apiService.suggestionAccount(
|
||||||
|
query: nil,
|
||||||
|
authenticationBox: authenticationBox
|
||||||
|
)
|
||||||
|
let userIDs = response2.value.map { $0.id }
|
||||||
|
userFetchedResultsController.userIDs = userIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,8 +70,6 @@ extension DiscoveryNewsViewModel.State {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
viewModel.links = []
|
|
||||||
|
|
||||||
stateMachine.enter(Loading.self)
|
stateMachine.enter(Loading.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +111,6 @@ extension DiscoveryNewsViewModel.State {
|
||||||
class Loading: DiscoveryNewsViewModel.State {
|
class Loading: DiscoveryNewsViewModel.State {
|
||||||
|
|
||||||
var offset: Int?
|
var offset: Int?
|
||||||
var isReloading: Bool { return offset == nil }
|
|
||||||
|
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
switch stateClass {
|
switch stateClass {
|
||||||
|
@ -146,6 +143,7 @@ extension DiscoveryNewsViewModel.State {
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.offset
|
let offset = self.offset
|
||||||
|
let isReloading = offset == nil
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
@ -169,7 +167,7 @@ extension DiscoveryNewsViewModel.State {
|
||||||
self.offset = newOffset
|
self.offset = newOffset
|
||||||
|
|
||||||
var hasNewItemsAppend = false
|
var hasNewItemsAppend = false
|
||||||
var links = viewModel.links
|
var links = isReloading ? [] : viewModel.links
|
||||||
for link in response.value {
|
for link in response.value {
|
||||||
guard !links.contains(link) else { continue }
|
guard !links.contains(link) else { continue }
|
||||||
links.append(link)
|
links.append(link)
|
||||||
|
|
|
@ -68,10 +68,7 @@ extension DiscoveryPostsViewModel.State {
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
// reset
|
|
||||||
viewModel.statusFetchedResultsController.statusIDs.value = []
|
|
||||||
|
|
||||||
stateMachine.enter(Loading.self)
|
stateMachine.enter(Loading.self)
|
||||||
}
|
}
|
||||||
|
@ -145,6 +142,7 @@ extension DiscoveryPostsViewModel.State {
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.offset
|
let offset = self.offset
|
||||||
|
let isReloading = offset == nil
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
@ -168,7 +166,7 @@ extension DiscoveryPostsViewModel.State {
|
||||||
self.offset = newOffset
|
self.offset = newOffset
|
||||||
|
|
||||||
var hasNewStatusesAppend = false
|
var hasNewStatusesAppend = false
|
||||||
var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value
|
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs.value
|
||||||
for status in response.value {
|
for status in response.value {
|
||||||
guard !statusIDs.contains(status.id) else { continue }
|
guard !statusIDs.contains(status.id) else { continue }
|
||||||
statusIDs.append(status.id)
|
statusIDs.append(status.id)
|
||||||
|
|
|
@ -13,10 +13,11 @@ import MastodonSDK
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
// please override this base class
|
// please override this base class
|
||||||
class ProfileViewModel: NSObject {
|
class ProfileViewModel: NSObject {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "ProfileViewModel", category: "ViewModel")
|
let logger = Logger(subsystem: "ProfileViewModel", category: "ViewModel")
|
||||||
|
|
||||||
typealias UserID = String
|
typealias UserID = String
|
||||||
|
@ -372,101 +373,6 @@ extension ProfileViewModel {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewModel {
|
|
||||||
|
|
||||||
enum RelationshipAction: Int, CaseIterable {
|
|
||||||
case none // set hide from UI
|
|
||||||
case follow
|
|
||||||
case request
|
|
||||||
case pending
|
|
||||||
case following
|
|
||||||
case muting
|
|
||||||
case blocked
|
|
||||||
case blocking
|
|
||||||
case suspended
|
|
||||||
case edit
|
|
||||||
case editing
|
|
||||||
case updating
|
|
||||||
|
|
||||||
var option: RelationshipActionOptionSet {
|
|
||||||
return RelationshipActionOptionSet(rawValue: 1 << rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct option set on the enum for safe iterator
|
|
||||||
struct RelationshipActionOptionSet: OptionSet {
|
|
||||||
let rawValue: Int
|
|
||||||
|
|
||||||
static let none = RelationshipAction.none.option
|
|
||||||
static let follow = RelationshipAction.follow.option
|
|
||||||
static let request = RelationshipAction.request.option
|
|
||||||
static let pending = RelationshipAction.pending.option
|
|
||||||
static let following = RelationshipAction.following.option
|
|
||||||
static let muting = RelationshipAction.muting.option
|
|
||||||
static let blocked = RelationshipAction.blocked.option
|
|
||||||
static let blocking = RelationshipAction.blocking.option
|
|
||||||
static let suspended = RelationshipAction.suspended.option
|
|
||||||
static let edit = RelationshipAction.edit.option
|
|
||||||
static let editing = RelationshipAction.editing.option
|
|
||||||
static let updating = RelationshipAction.updating.option
|
|
||||||
|
|
||||||
static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating]
|
|
||||||
|
|
||||||
func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
|
|
||||||
let set = subtracting(except)
|
|
||||||
for action in RelationshipAction.allCases.reversed() where set.contains(action.option) {
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
guard let highPriorityAction = self.highPriorityAction(except: []) else {
|
|
||||||
assertionFailure()
|
|
||||||
return " "
|
|
||||||
}
|
|
||||||
switch highPriorityAction {
|
|
||||||
case .none: return " "
|
|
||||||
case .follow: return L10n.Common.Controls.Friendship.follow
|
|
||||||
case .request: return L10n.Common.Controls.Friendship.request
|
|
||||||
case .pending: return L10n.Common.Controls.Friendship.pending
|
|
||||||
case .following: return L10n.Common.Controls.Friendship.following
|
|
||||||
case .muting: return L10n.Common.Controls.Friendship.muted
|
|
||||||
case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user
|
|
||||||
case .blocking: return L10n.Common.Controls.Friendship.blocked
|
|
||||||
case .suspended: return L10n.Common.Controls.Friendship.follow
|
|
||||||
case .edit: return L10n.Common.Controls.Friendship.editInfo
|
|
||||||
case .editing: return L10n.Common.Controls.Actions.done
|
|
||||||
case .updating: return " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, deprecated, message: "")
|
|
||||||
var backgroundColor: UIColor {
|
|
||||||
guard let highPriorityAction = self.highPriorityAction(except: []) else {
|
|
||||||
assertionFailure()
|
|
||||||
return Asset.Colors.brandBlue.color
|
|
||||||
}
|
|
||||||
switch highPriorityAction {
|
|
||||||
case .none: return Asset.Colors.brandBlue.color
|
|
||||||
case .follow: return Asset.Colors.brandBlue.color
|
|
||||||
case .request: return Asset.Colors.brandBlue.color
|
|
||||||
case .pending: return Asset.Colors.brandBlue.color
|
|
||||||
case .following: return Asset.Colors.brandBlue.color
|
|
||||||
case .muting: return Asset.Colors.alertYellow.color
|
|
||||||
case .blocked: return Asset.Colors.brandBlue.color
|
|
||||||
case .blocking: return Asset.Colors.danger.color
|
|
||||||
case .suspended: return Asset.Colors.brandBlue.color
|
|
||||||
case .edit: return Asset.Colors.brandBlue.color
|
|
||||||
case .editing: return Asset.Colors.brandBlue.color
|
|
||||||
case .updating: return Asset.Colors.brandBlue.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewModel {
|
extension ProfileViewModel {
|
||||||
func updateProfileInfo(
|
func updateProfileInfo(
|
||||||
headerProfileInfo: ProfileHeaderViewModel.ProfileInfo,
|
headerProfileInfo: ProfileHeaderViewModel.ProfileInfo,
|
||||||
|
|
|
@ -13,12 +13,13 @@ import CoreDataStack
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
extension APIService {
|
extension APIService {
|
||||||
|
|
||||||
func suggestionAccount(
|
func suggestionAccount(
|
||||||
query: Mastodon.API.Suggestions.Query?,
|
query: Mastodon.API.Suggestions.Query?,
|
||||||
authenticationBox: MastodonAuthenticationBox
|
authenticationBox: MastodonAuthenticationBox
|
||||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
|
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
|
||||||
|
|
||||||
let response = try await Mastodon.API.Suggestions.get(
|
let response = try await Mastodon.API.Suggestions.accounts(
|
||||||
session: session,
|
session: session,
|
||||||
domain: authenticationBox.domain,
|
domain: authenticationBox.domain,
|
||||||
query: query,
|
query: query,
|
||||||
|
@ -47,7 +48,7 @@ extension APIService {
|
||||||
query: Mastodon.API.Suggestions.Query?,
|
query: Mastodon.API.Suggestions.Query?,
|
||||||
authenticationBox: MastodonAuthenticationBox
|
authenticationBox: MastodonAuthenticationBox
|
||||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]> {
|
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]> {
|
||||||
let response = try await Mastodon.API.V2.Suggestions.get(
|
let response = try await Mastodon.API.V2.Suggestions.accounts(
|
||||||
session: session,
|
session: session,
|
||||||
domain: authenticationBox.domain,
|
domain: authenticationBox.domain,
|
||||||
query: query,
|
query: query,
|
||||||
|
|
|
@ -94,6 +94,7 @@ let package = Package(
|
||||||
.product(name: "Alamofire", package: "Alamofire"),
|
.product(name: "Alamofire", package: "Alamofire"),
|
||||||
.product(name: "AlamofireImage", package: "AlamofireImage"),
|
.product(name: "AlamofireImage", package: "AlamofireImage"),
|
||||||
.product(name: "MetaTextKit", package: "MetaTextKit"),
|
.product(name: "MetaTextKit", package: "MetaTextKit"),
|
||||||
|
.product(name: "MastodonMeta", package: "MetaTextKit"),
|
||||||
.product(name: "FLAnimatedImage", package: "FLAnimatedImage"),
|
.product(name: "FLAnimatedImage", package: "FLAnimatedImage"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "251",
|
||||||
|
"green" : "250",
|
||||||
|
"red" : "249"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "4",
|
||||||
|
"green" : "5",
|
||||||
|
"red" : "6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,6 +103,9 @@ public enum Asset {
|
||||||
public static let star = ImageAsset(name: "ObjectsAndTools/star")
|
public static let star = ImageAsset(name: "ObjectsAndTools/star")
|
||||||
}
|
}
|
||||||
public enum Scene {
|
public enum Scene {
|
||||||
|
public enum Discovery {
|
||||||
|
public static let profileCardBackground = ColorAsset(name: "Scene/Discovery/profile.card.background")
|
||||||
|
}
|
||||||
public enum Onboarding {
|
public enum Onboarding {
|
||||||
public static let avatarPlaceholder = ImageAsset(name: "Scene/Onboarding/avatar.placeholder")
|
public static let avatarPlaceholder = ImageAsset(name: "Scene/Onboarding/avatar.placeholder")
|
||||||
public static let background = ColorAsset(name: "Scene/Onboarding/background")
|
public static let background = ColorAsset(name: "Scene/Onboarding/background")
|
||||||
|
|
|
@ -9,7 +9,7 @@ import UIKit
|
||||||
|
|
||||||
extension UserDefaults {
|
extension UserDefaults {
|
||||||
|
|
||||||
@objc dynamic var customUserInterfaceStyle: UIUserInterfaceStyle {
|
@objc public dynamic var customUserInterfaceStyle: UIUserInterfaceStyle {
|
||||||
get {
|
get {
|
||||||
register(defaults: [#function: UIUserInterfaceStyle.unspecified.rawValue])
|
register(defaults: [#function: UIUserInterfaceStyle.unspecified.rawValue])
|
||||||
return UIUserInterfaceStyle(rawValue: integer(forKey: #function)) ?? .unspecified
|
return UIUserInterfaceStyle(rawValue: integer(forKey: #function)) ?? .unspecified
|
||||||
|
@ -17,7 +17,7 @@ extension UserDefaults {
|
||||||
set { self[#function] = newValue.rawValue }
|
set { self[#function] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc dynamic var preferredStaticAvatar: Bool {
|
@objc public dynamic var preferredStaticAvatar: Bool {
|
||||||
get {
|
get {
|
||||||
// default false
|
// default false
|
||||||
// without set register to profile timeline performance
|
// without set register to profile timeline performance
|
||||||
|
@ -26,7 +26,7 @@ extension UserDefaults {
|
||||||
set { self[#function] = newValue }
|
set { self[#function] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc dynamic var preferredStaticEmoji: Bool {
|
@objc public dynamic var preferredStaticEmoji: Bool {
|
||||||
get {
|
get {
|
||||||
// default false
|
// default false
|
||||||
// without set register to profile timeline performance
|
// without set register to profile timeline performance
|
|
@ -27,7 +27,7 @@ extension Mastodon.API.Suggestions {
|
||||||
/// - query: query
|
/// - query: query
|
||||||
/// - authorization: User token.
|
/// - authorization: User token.
|
||||||
/// - Returns: `AnyPublisher` contains `Accounts` nested in the response
|
/// - Returns: `AnyPublisher` contains `Accounts` nested in the response
|
||||||
public static func get(
|
public static func accounts(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
query: Mastodon.API.Suggestions.Query?,
|
query: Mastodon.API.Suggestions.Query?,
|
||||||
|
|
|
@ -20,7 +20,7 @@ extension Mastodon.API.V2.Suggestions {
|
||||||
/// - query: query
|
/// - query: query
|
||||||
/// - authorization: User token.
|
/// - authorization: User token.
|
||||||
/// - Returns: `AnyPublisher` contains `AccountsSuggestion` nested in the response
|
/// - Returns: `AnyPublisher` contains `AccountsSuggestion` nested in the response
|
||||||
public static func get(
|
public static func accounts(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
query: Mastodon.API.Suggestions.Query?,
|
query: Mastodon.API.Suggestions.Query?,
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// MastodonEmoji.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
|
extension Collection where Element == MastodonEmoji {
|
||||||
|
public var asDictionary: MastodonContent.Emojis {
|
||||||
|
var dictionary: MastodonContent.Emojis = [:]
|
||||||
|
for emoji in self {
|
||||||
|
dictionary[emoji.code] = emoji.url
|
||||||
|
}
|
||||||
|
return dictionary
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
// MastodonUser.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonCommon
|
||||||
|
|
||||||
|
extension MastodonUser {
|
||||||
|
|
||||||
|
public var displayNameWithFallback: String {
|
||||||
|
return !displayName.isEmpty ? displayName : username
|
||||||
|
}
|
||||||
|
|
||||||
|
public var acctWithDomain: String {
|
||||||
|
if !acct.contains("@") {
|
||||||
|
// Safe concat due to username cannot contains "@"
|
||||||
|
return username + "@" + domain
|
||||||
|
} else {
|
||||||
|
return acct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var domainFromAcct: String {
|
||||||
|
if !acct.contains("@") {
|
||||||
|
return domain
|
||||||
|
} else {
|
||||||
|
let domain = acct.split(separator: "@").last
|
||||||
|
return String(domain!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonUser {
|
||||||
|
|
||||||
|
public func headerImageURL() -> URL? {
|
||||||
|
return URL(string: header)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func headerImageURLWithFallback(domain: String) -> URL {
|
||||||
|
return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")!
|
||||||
|
}
|
||||||
|
|
||||||
|
public func avatarImageURL() -> URL? {
|
||||||
|
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||||
|
return URL(string: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||||
|
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ extension MetaLabel {
|
||||||
case notificationTitle
|
case notificationTitle
|
||||||
case profileFieldName
|
case profileFieldName
|
||||||
case profileFieldValue
|
case profileFieldValue
|
||||||
|
case profileCardName
|
||||||
|
case profileCardUsername
|
||||||
case recommendAccountName
|
case recommendAccountName
|
||||||
case titleView
|
case titleView
|
||||||
case settingTableFooter
|
case settingTableFooter
|
||||||
|
@ -51,7 +53,7 @@ extension MetaLabel {
|
||||||
textColor = Asset.Colors.Label.secondary.color
|
textColor = Asset.Colors.Label.secondary.color
|
||||||
|
|
||||||
case .statusName:
|
case .statusName:
|
||||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))
|
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||||
textColor = Asset.Colors.Label.primary.color
|
textColor = Asset.Colors.Label.primary.color
|
||||||
|
|
||||||
case .statusUsername:
|
case .statusUsername:
|
||||||
|
@ -80,6 +82,14 @@ extension MetaLabel {
|
||||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||||
textColor = Asset.Colors.Label.primary.color
|
textColor = Asset.Colors.Label.primary.color
|
||||||
|
|
||||||
|
case .profileCardName:
|
||||||
|
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
|
textColor = Asset.Colors.Label.primary.color
|
||||||
|
|
||||||
|
case .profileCardUsername:
|
||||||
|
font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
textColor = Asset.Colors.Label.secondary.color
|
||||||
|
|
||||||
case .titleView:
|
case .titleView:
|
||||||
font = .systemFont(ofSize: 17, weight: .semibold)
|
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
textColor = Asset.Colors.Label.primary.color
|
textColor = Asset.Colors.Label.primary.color
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final public class MastodonMetricFormatter: Formatter {
|
public final class MastodonMetricFormatter: Formatter {
|
||||||
|
|
||||||
public func string(from number: Int) -> String? {
|
public func string(from number: Int) -> String? {
|
||||||
let isPositive = number >= 0
|
let isPositive = number >= 0
|
||||||
let symbol = isPositive ? "" : "-"
|
let symbol = isPositive ? "" : "-"
|
|
@ -0,0 +1,88 @@
|
||||||
|
//
|
||||||
|
// ProfileCardView+Configuration.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
|
import Meta
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
|
extension ProfileCardView {
|
||||||
|
|
||||||
|
public func configure(user: MastodonUser) {
|
||||||
|
// banner
|
||||||
|
user.publisher(for: \.header)
|
||||||
|
.map { URL(string: $0) }
|
||||||
|
.assign(to: \.authorBannerImageURL, on: viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// author avatar
|
||||||
|
Publishers.CombineLatest3(
|
||||||
|
user.publisher(for: \.avatar),
|
||||||
|
user.publisher(for: \.avatarStatic),
|
||||||
|
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
|
||||||
|
)
|
||||||
|
.map { _ in user.avatarImageURL() }
|
||||||
|
.assign(to: \.authorAvatarImageURL, on: viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// 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)
|
||||||
|
// username
|
||||||
|
user.publisher(for: \.acct)
|
||||||
|
.map { $0 as String? }
|
||||||
|
.assign(to: \.authorUsername, on: viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// bio
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
user.publisher(for: \.note),
|
||||||
|
user.publisher(for: \.emojis)
|
||||||
|
)
|
||||||
|
.map { note, emojis in
|
||||||
|
guard let note = note else { return nil }
|
||||||
|
do {
|
||||||
|
let content = MastodonContent(content: note, emojis: emojis.asDictionary)
|
||||||
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||||
|
return metaContent
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.assign(to: \.bioContent, on: viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// relationship
|
||||||
|
viewModel.relationshipViewModel.user = user
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
//
|
||||||
|
// ProfileCardView+ViewModel.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import Meta
|
||||||
|
import AlamofireImage
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
|
extension ProfileCardView {
|
||||||
|
public class ViewModel: ObservableObject {
|
||||||
|
let logger = Logger(subsystem: "ProfileCardView", category: "ViewModel")
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
public let relationshipViewModel = RelationshipViewModel()
|
||||||
|
|
||||||
|
// Author
|
||||||
|
@Published public var authorBannerImageURL: URL?
|
||||||
|
@Published public var authorAvatarImageURL: URL?
|
||||||
|
@Published public var authorName: MetaContent?
|
||||||
|
@Published public var authorUsername: String?
|
||||||
|
|
||||||
|
@Published public var bioContent: MetaContent?
|
||||||
|
|
||||||
|
@Published public var statusesCount: Int?
|
||||||
|
@Published public var followingCount: Int?
|
||||||
|
@Published public var followersCount: Int?
|
||||||
|
|
||||||
|
@Published public var isUpdating = false
|
||||||
|
@Published public var isFollowedBy = false
|
||||||
|
@Published public var isMuting = false
|
||||||
|
@Published public var isBlocking = false
|
||||||
|
@Published public var isBlockedBy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileCardView.ViewModel {
|
||||||
|
func bind(view: ProfileCardView) {
|
||||||
|
bindHeader(view: view)
|
||||||
|
bindUser(view: view)
|
||||||
|
bindBio(view: view)
|
||||||
|
bindRelationship(view: view)
|
||||||
|
bindDashboard(view: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindHeader(view: ProfileCardView) {
|
||||||
|
$authorBannerImageURL
|
||||||
|
.sink { url in
|
||||||
|
guard let url = url else { return }
|
||||||
|
view.bannerImageView.af.setImage(
|
||||||
|
withURL: url,
|
||||||
|
placeholderImage: .placeholder(color: .systemGray3),
|
||||||
|
imageTransition: .crossDissolve(0.3)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindUser(view: ProfileCardView) {
|
||||||
|
$authorAvatarImageURL
|
||||||
|
.sink { url in
|
||||||
|
view.avatarButton.avatarImageView.configure(
|
||||||
|
configuration: .init(
|
||||||
|
url: url,
|
||||||
|
placeholder: .placeholder(color: .systemGray3)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
view.avatarButton.avatarImageView.configure(
|
||||||
|
cornerConfiguration: .init(corner: .fixed(radius: 12))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// name
|
||||||
|
$authorName
|
||||||
|
.sink { metaContent in
|
||||||
|
let metaContent = metaContent ?? PlaintextMetaContent(string: " ")
|
||||||
|
view.authorNameLabel.configure(content: metaContent)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// username
|
||||||
|
$authorUsername
|
||||||
|
.map { text -> String in
|
||||||
|
guard let text = text else { return "" }
|
||||||
|
return "@\(text)"
|
||||||
|
}
|
||||||
|
.sink { username in
|
||||||
|
let metaContent = PlaintextMetaContent(string: username)
|
||||||
|
view.authorUsernameLabel.configure(content: metaContent)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindBio(view: ProfileCardView) {
|
||||||
|
$bioContent
|
||||||
|
.sink { metaContent in
|
||||||
|
let metaContent = metaContent ?? PlaintextMetaContent(string: " ")
|
||||||
|
view.bioMetaText.configure(content: metaContent)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindRelationship(view: ProfileCardView) {
|
||||||
|
relationshipViewModel.$optionSet
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { relationshipActionSet in
|
||||||
|
let relationshipActionSet = relationshipActionSet ?? .follow
|
||||||
|
view.relationshipActionButton.configure(actionOptionSet: relationshipActionSet)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindDashboard(view: ProfileCardView) {
|
||||||
|
$statusesCount
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { count in
|
||||||
|
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||||
|
view.statusDashboardView.postDashboardMeterView.numberLabel.text = text
|
||||||
|
view.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true
|
||||||
|
view.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Plural.Count.post(count ?? 0)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
$followingCount
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { count in
|
||||||
|
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||||
|
view.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
|
||||||
|
view.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true
|
||||||
|
view.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Plural.Count.following(count ?? 0)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
$followersCount
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { count in
|
||||||
|
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||||
|
view.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
|
||||||
|
view.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true
|
||||||
|
view.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Plural.Count.follower(count ?? 0)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
//
|
||||||
|
// ProfileCardView.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import MetaTextKit
|
||||||
|
import MastodonAsset
|
||||||
|
|
||||||
|
public final class ProfileCardView: UIView {
|
||||||
|
|
||||||
|
static let avatarSize = CGSize(width: 56, height: 56)
|
||||||
|
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
|
||||||
|
static let contentMargin: CGFloat = 16
|
||||||
|
|
||||||
|
private var _disposeBag = Set<AnyCancellable>()
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let container = UIStackView()
|
||||||
|
|
||||||
|
let bannerImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
imageView.layer.cornerRadius = 3
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
// avatar
|
||||||
|
public let avatarButton = AvatarButton()
|
||||||
|
|
||||||
|
// author name
|
||||||
|
public let authorNameLabel = MetaLabel(style: .profileCardName)
|
||||||
|
|
||||||
|
// author username
|
||||||
|
public let authorUsernameLabel = MetaLabel(style: .profileCardUsername)
|
||||||
|
|
||||||
|
let bioMetaText: MetaText = {
|
||||||
|
let metaText = MetaText()
|
||||||
|
metaText.textView.backgroundColor = .clear
|
||||||
|
metaText.textView.isEditable = false
|
||||||
|
metaText.textView.isSelectable = true
|
||||||
|
metaText.textView.isScrollEnabled = false
|
||||||
|
//metaText.textView.textContainer.lineFragmentPadding = 0
|
||||||
|
//metaText.textView.textContainerInset = .zero
|
||||||
|
metaText.textView.layer.masksToBounds = false
|
||||||
|
metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
|
||||||
|
|
||||||
|
metaText.textView.layer.masksToBounds = true
|
||||||
|
metaText.textView.layer.cornerCurve = .continuous
|
||||||
|
metaText.textView.layer.cornerRadius = 10
|
||||||
|
|
||||||
|
metaText.paragraphStyle = {
|
||||||
|
let style = NSMutableParagraphStyle()
|
||||||
|
style.lineSpacing = 5
|
||||||
|
style.paragraphSpacing = 8
|
||||||
|
return style
|
||||||
|
}()
|
||||||
|
metaText.textAttributes = [
|
||||||
|
.font: UIFont.preferredFont(forTextStyle: .body),
|
||||||
|
.foregroundColor: Asset.Colors.Label.primary.color,
|
||||||
|
]
|
||||||
|
metaText.linkAttributes = [
|
||||||
|
.font: UIFont.preferredFont(forTextStyle: .body),
|
||||||
|
.foregroundColor: Asset.Colors.brandBlue.color,
|
||||||
|
]
|
||||||
|
return metaText
|
||||||
|
}()
|
||||||
|
|
||||||
|
let statusDashboardView = ProfileStatusDashboardView()
|
||||||
|
|
||||||
|
let relationshipActionButtonShadowContainer = ShadowBackgroundContainer()
|
||||||
|
let relationshipActionButton: ProfileRelationshipActionButton = {
|
||||||
|
let button = ProfileRelationshipActionButton()
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
|
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||||
|
button.titleLabel?.minimumScaleFactor = 0.5
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var viewModel: ViewModel = {
|
||||||
|
let viewModel = ViewModel()
|
||||||
|
viewModel.bind(view: self)
|
||||||
|
return viewModel
|
||||||
|
}()
|
||||||
|
|
||||||
|
public func prepareForReuse() {
|
||||||
|
disposeBag.removeAll()
|
||||||
|
bannerImageView.af.cancelImageRequest()
|
||||||
|
bannerImageView.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileCardView {
|
||||||
|
private func _init() {
|
||||||
|
avatarButton.isUserInteractionEnabled = false
|
||||||
|
authorNameLabel.isUserInteractionEnabled = false
|
||||||
|
authorUsernameLabel.isUserInteractionEnabled = false
|
||||||
|
bioMetaText.textView.isUserInteractionEnabled = false
|
||||||
|
statusDashboardView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
// container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer ]
|
||||||
|
container.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
|
||||||
|
container.axis = .vertical
|
||||||
|
container.spacing = 8
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(container)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
container.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
container.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
container.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
// bannerContainer
|
||||||
|
let bannerContainer = UIView()
|
||||||
|
bannerContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addArrangedSubview(bannerContainer)
|
||||||
|
container.setCustomSpacing(6, after: bannerContainer)
|
||||||
|
|
||||||
|
// bannerImageView
|
||||||
|
bannerImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
bannerContainer.addSubview(bannerImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bannerImageView.topAnchor.constraint(equalTo: bannerContainer.topAnchor, constant: 4),
|
||||||
|
bannerImageView.leadingAnchor.constraint(equalTo: bannerContainer.leadingAnchor, constant: 4),
|
||||||
|
bannerContainer.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor, constant: 4),
|
||||||
|
bannerImageView.bottomAnchor.constraint(equalTo: bannerContainer.bottomAnchor),
|
||||||
|
bannerImageView.widthAnchor.constraint(equalTo: bannerImageView.heightAnchor, multiplier: 335.0/128.0).priority(.required - 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
// authorContainer: H - [ avatarPlaceholder | authorInfoContainer ]
|
||||||
|
let authorContainer = UIStackView()
|
||||||
|
authorContainer.axis = .horizontal
|
||||||
|
authorContainer.spacing = 16
|
||||||
|
let authorContainerAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
|
authorContainerAdaptiveMarginContainerView.contentView = authorContainer
|
||||||
|
authorContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
|
||||||
|
container.addArrangedSubview(authorContainerAdaptiveMarginContainerView)
|
||||||
|
|
||||||
|
// avatarPlaceholder
|
||||||
|
let avatarPlaceholder = UIView()
|
||||||
|
avatarPlaceholder.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
authorContainer.addArrangedSubview(avatarPlaceholder)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarPlaceholder.widthAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.width).priority(.required - 1),
|
||||||
|
avatarPlaceholder.heightAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.height - 14).priority(.required - 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
authorContainer.addSubview(avatarButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarButton.leadingAnchor.constraint(equalTo: avatarPlaceholder.leadingAnchor),
|
||||||
|
avatarButton.trailingAnchor.constraint(equalTo: avatarPlaceholder.trailingAnchor),
|
||||||
|
avatarButton.bottomAnchor.constraint(equalTo: avatarPlaceholder.bottomAnchor),
|
||||||
|
avatarButton.heightAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.height).priority(.required - 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
let avatarButtonBackgroundView = UIView()
|
||||||
|
avatarButtonBackgroundView.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
|
||||||
|
avatarButtonBackgroundView.layer.masksToBounds = true
|
||||||
|
avatarButtonBackgroundView.layer.cornerCurve = .continuous
|
||||||
|
avatarButtonBackgroundView.layer.cornerRadius = 12
|
||||||
|
avatarButtonBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
authorContainer.insertSubview(avatarButtonBackgroundView, belowSubview: avatarButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarButtonBackgroundView.centerXAnchor.constraint(equalTo: avatarButton.centerXAnchor),
|
||||||
|
avatarButtonBackgroundView.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor),
|
||||||
|
avatarButtonBackgroundView.widthAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.width + 4).priority(.required - 1),
|
||||||
|
avatarButtonBackgroundView.heightAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.height + 4).priority(.required - 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
// authorInfoContainer: V - [ authorNameLabel | authorUsernameLabel ]
|
||||||
|
let authorInfoContainer = UIStackView()
|
||||||
|
authorInfoContainer.axis = .vertical
|
||||||
|
authorInfoContainer.spacing = 2
|
||||||
|
authorContainer.addArrangedSubview(authorInfoContainer)
|
||||||
|
|
||||||
|
authorInfoContainer.addArrangedSubview(authorNameLabel)
|
||||||
|
authorInfoContainer.addArrangedSubview(authorUsernameLabel)
|
||||||
|
|
||||||
|
// bioMetaText
|
||||||
|
let bioMetaTextAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
|
bioMetaTextAdaptiveMarginContainerView.contentView = bioMetaText.textView
|
||||||
|
bioMetaTextAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
|
||||||
|
container.addArrangedSubview(bioMetaTextAdaptiveMarginContainerView)
|
||||||
|
container.setCustomSpacing(16, after: bioMetaTextAdaptiveMarginContainerView)
|
||||||
|
|
||||||
|
// infoContainer: H - [ statusDashboardView | (spacer) | relationshipActionButton ]
|
||||||
|
let infoContainer = UIStackView()
|
||||||
|
infoContainer.axis = .horizontal
|
||||||
|
let infoContainerAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
|
infoContainerAdaptiveMarginContainerView.contentView = infoContainer
|
||||||
|
infoContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
|
||||||
|
container.addArrangedSubview(infoContainerAdaptiveMarginContainerView)
|
||||||
|
infoContainer.addArrangedSubview(statusDashboardView)
|
||||||
|
infoContainer.addArrangedSubview(UIView())
|
||||||
|
let relationshipActionButtonShadowContainer = ShadowBackgroundContainer()
|
||||||
|
infoContainer.addArrangedSubview(relationshipActionButtonShadowContainer)
|
||||||
|
|
||||||
|
relationshipActionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
relationshipActionButtonShadowContainer.addSubview(relationshipActionButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
relationshipActionButton.topAnchor.constraint(equalTo: relationshipActionButtonShadowContainer.topAnchor),
|
||||||
|
relationshipActionButton.leadingAnchor.constraint(equalTo: relationshipActionButtonShadowContainer.leadingAnchor),
|
||||||
|
relationshipActionButton.trailingAnchor.constraint(equalTo: relationshipActionButtonShadowContainer.trailingAnchor),
|
||||||
|
relationshipActionButton.bottomAnchor.constraint(equalTo: relationshipActionButtonShadowContainer.bottomAnchor),
|
||||||
|
relationshipActionButton.widthAnchor.constraint(greaterThanOrEqualToConstant: ProfileCardView.friendshipActionButtonSize.width).priority(.required - 1),
|
||||||
|
relationshipActionButton.heightAnchor.constraint(equalToConstant: ProfileCardView.friendshipActionButtonSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
let bottomPadding = UIView()
|
||||||
|
bottomPadding.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addArrangedSubview(bottomPadding)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomPadding.heightAnchor.constraint(equalToConstant: 16)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import MastodonSDK
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import MastodonExtension
|
import MastodonExtension
|
||||||
|
import MastodonCommon
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
|
||||||
extension StatusView {
|
extension StatusView {
|
||||||
|
|
|
@ -6,23 +6,23 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonUI
|
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
final class ProfileRelationshipActionButton: RoundedEdgesButton {
|
public final class ProfileRelationshipActionButton: RoundedEdgesButton {
|
||||||
|
|
||||||
let activityIndicatorView: UIActivityIndicatorView = {
|
public let activityIndicatorView: UIActivityIndicatorView = {
|
||||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||||
activityIndicatorView.color = Asset.Colors.Label.primaryReverse.color
|
activityIndicatorView.color = Asset.Colors.Label.primaryReverse.color
|
||||||
return activityIndicatorView
|
return activityIndicatorView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ extension ProfileRelationshipActionButton {
|
||||||
configureAppearance()
|
configureAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
configureAppearance()
|
configureAppearance()
|
||||||
|
@ -55,7 +55,7 @@ extension ProfileRelationshipActionButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileRelationshipActionButton {
|
extension ProfileRelationshipActionButton {
|
||||||
func configure(actionOptionSet: ProfileViewModel.RelationshipActionOptionSet) {
|
public func configure(actionOptionSet: RelationshipActionOptionSet) {
|
||||||
setTitle(actionOptionSet.title, for: .normal)
|
setTitle(actionOptionSet.title, for: .normal)
|
||||||
|
|
||||||
configureAppearance()
|
configureAppearance()
|
||||||
|
@ -87,9 +87,5 @@ extension ProfileRelationshipActionButton {
|
||||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted)
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted)
|
||||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled)
|
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled)
|
||||||
}
|
}
|
||||||
// setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor), for: .normal)
|
|
||||||
// setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
|
|
||||||
// setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .disabled)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import UIKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
final class ProfileStatusDashboardMeterView: UIView {
|
public final class ProfileStatusDashboardMeterView: UIView {
|
||||||
|
|
||||||
let numberLabel: UILabel = {
|
public let numberLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = {
|
label.font = {
|
||||||
let font = UIFont.systemFont(ofSize: 20, weight: .semibold)
|
let font = UIFont.systemFont(ofSize: 20, weight: .semibold)
|
||||||
|
@ -25,7 +25,7 @@ final class ProfileStatusDashboardMeterView: UIView {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let textLabel: UILabel = {
|
public let textLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
@ -41,12 +41,12 @@ final class ProfileStatusDashboardMeterView: UIView {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
|
@ -10,24 +10,24 @@ import UIKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
protocol ProfileStatusDashboardViewDelegate: AnyObject {
|
public protocol ProfileStatusDashboardViewDelegate: AnyObject {
|
||||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter)
|
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ProfileStatusDashboardView: UIView {
|
public final class ProfileStatusDashboardView: UIView {
|
||||||
|
|
||||||
let postDashboardMeterView = ProfileStatusDashboardMeterView()
|
public let postDashboardMeterView = ProfileStatusDashboardMeterView()
|
||||||
let followingDashboardMeterView = ProfileStatusDashboardMeterView()
|
public let followingDashboardMeterView = ProfileStatusDashboardMeterView()
|
||||||
let followersDashboardMeterView = ProfileStatusDashboardMeterView()
|
public let followersDashboardMeterView = ProfileStatusDashboardMeterView()
|
||||||
|
|
||||||
weak var delegate: ProfileStatusDashboardViewDelegate?
|
public weak var delegate: ProfileStatusDashboardViewDelegate?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ final class ProfileStatusDashboardView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusDashboardView {
|
extension ProfileStatusDashboardView {
|
||||||
enum Meter: Hashable {
|
public enum Meter: Hashable {
|
||||||
case post
|
case post
|
||||||
case following
|
case following
|
||||||
case follower
|
case follower
|
||||||
|
@ -83,7 +83,7 @@ extension ProfileStatusDashboardView {
|
||||||
|
|
||||||
extension ProfileStatusDashboardView {
|
extension ProfileStatusDashboardView {
|
||||||
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
guard let sourceView = sender.view as? ProfileStatusDashboardMeterView else {
|
guard let sourceView = sender.view as? ProfileStatusDashboardMeterView else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
|
@ -0,0 +1,68 @@
|
||||||
|
//
|
||||||
|
// ProfileCardTableViewCell.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public final class ProfileCardTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
public var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
public let profileCardView: ProfileCardView = {
|
||||||
|
let profileCardView = ProfileCardView()
|
||||||
|
profileCardView.layer.masksToBounds = true
|
||||||
|
profileCardView.layer.cornerRadius = 6
|
||||||
|
profileCardView.layer.cornerCurve = .continuous
|
||||||
|
return profileCardView
|
||||||
|
}()
|
||||||
|
|
||||||
|
public override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
disposeBag.removeAll()
|
||||||
|
profileCardView.prepareForReuse()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileCardTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
|
|
||||||
|
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
||||||
|
shadowBackgroundContainer.cornerRadius = 6
|
||||||
|
shadowBackgroundContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(shadowBackgroundContainer)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
shadowBackgroundContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||||
|
shadowBackgroundContainer.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
shadowBackgroundContainer.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor, constant: 10),
|
||||||
|
])
|
||||||
|
|
||||||
|
profileCardView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(profileCardView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
profileCardView.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor),
|
||||||
|
profileCardView.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor),
|
||||||
|
profileCardView.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor),
|
||||||
|
profileCardView.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,253 @@
|
||||||
|
//
|
||||||
|
// RelationshipViewModel.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
public enum RelationshipAction: Int, CaseIterable {
|
||||||
|
case isMyself
|
||||||
|
case followingBy
|
||||||
|
case blockingBy
|
||||||
|
case none // set hide from UI
|
||||||
|
case follow
|
||||||
|
case request
|
||||||
|
case pending
|
||||||
|
case following
|
||||||
|
case muting
|
||||||
|
case blocked
|
||||||
|
case blocking
|
||||||
|
case suspended
|
||||||
|
case edit
|
||||||
|
case editing
|
||||||
|
case updating
|
||||||
|
|
||||||
|
public var option: RelationshipActionOptionSet {
|
||||||
|
return RelationshipActionOptionSet(rawValue: 1 << rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct option set on the enum for safe iterator
|
||||||
|
public struct RelationshipActionOptionSet: OptionSet {
|
||||||
|
|
||||||
|
public let rawValue: Int
|
||||||
|
|
||||||
|
public init(rawValue: Int) {
|
||||||
|
self.rawValue = rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let isMyself = RelationshipAction.isMyself.option
|
||||||
|
public static let followingBy = RelationshipAction.followingBy.option
|
||||||
|
public static let blockingBy = RelationshipAction.blockingBy.option
|
||||||
|
public static let none = RelationshipAction.none.option
|
||||||
|
public static let follow = RelationshipAction.follow.option
|
||||||
|
public static let request = RelationshipAction.request.option
|
||||||
|
public static let pending = RelationshipAction.pending.option
|
||||||
|
public static let following = RelationshipAction.following.option
|
||||||
|
public static let muting = RelationshipAction.muting.option
|
||||||
|
public static let blocked = RelationshipAction.blocked.option
|
||||||
|
public static let blocking = RelationshipAction.blocking.option
|
||||||
|
public static let suspended = RelationshipAction.suspended.option
|
||||||
|
public static let edit = RelationshipAction.edit.option
|
||||||
|
public static let editing = RelationshipAction.editing.option
|
||||||
|
public static let updating = RelationshipAction.updating.option
|
||||||
|
|
||||||
|
public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating]
|
||||||
|
|
||||||
|
public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
|
||||||
|
let set = subtracting(except)
|
||||||
|
for action in RelationshipAction.allCases.reversed() where set.contains(action.option) {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var title: String {
|
||||||
|
guard let highPriorityAction = self.highPriorityAction(except: []) else {
|
||||||
|
assertionFailure()
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
switch highPriorityAction {
|
||||||
|
case .isMyself: return ""
|
||||||
|
case .followingBy: return " "
|
||||||
|
case .blockingBy: return " "
|
||||||
|
case .none: return " "
|
||||||
|
case .follow: return L10n.Common.Controls.Friendship.follow
|
||||||
|
case .request: return L10n.Common.Controls.Friendship.request
|
||||||
|
case .pending: return L10n.Common.Controls.Friendship.pending
|
||||||
|
case .following: return L10n.Common.Controls.Friendship.following
|
||||||
|
case .muting: return L10n.Common.Controls.Friendship.muted
|
||||||
|
case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user
|
||||||
|
case .blocking: return L10n.Common.Controls.Friendship.blocked
|
||||||
|
case .suspended: return L10n.Common.Controls.Friendship.follow
|
||||||
|
case .edit: return L10n.Common.Controls.Friendship.editInfo
|
||||||
|
case .editing: return L10n.Common.Controls.Actions.done
|
||||||
|
case .updating: return " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class RelationshipViewModel {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
public var userObserver: AnyCancellable?
|
||||||
|
public var meObserver: AnyCancellable?
|
||||||
|
|
||||||
|
// input
|
||||||
|
@Published public var user: MastodonUser?
|
||||||
|
@Published public var me: MastodonUser?
|
||||||
|
public let relationshipUpdatePublisher = CurrentValueSubject<Void, Never>(Void()) // needs initial event
|
||||||
|
|
||||||
|
// output
|
||||||
|
@Published public var isMyself = false
|
||||||
|
@Published public var optionSet: RelationshipActionOptionSet?
|
||||||
|
|
||||||
|
@Published public var isFollowing = false
|
||||||
|
@Published public var isFollowingBy = false
|
||||||
|
@Published public var isMuting = false
|
||||||
|
@Published public var isBlocking = false
|
||||||
|
@Published public var isBlockingBy = false
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
Publishers.CombineLatest3(
|
||||||
|
$user,
|
||||||
|
$me,
|
||||||
|
relationshipUpdatePublisher
|
||||||
|
)
|
||||||
|
.sink { [weak self] user, me, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.update(user: user, me: me)
|
||||||
|
|
||||||
|
guard let user = user, let me = me else {
|
||||||
|
self.userObserver = nil
|
||||||
|
self.meObserver = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not modify object to prevent infinity loop
|
||||||
|
self.userObserver = RelationshipViewModel.createObjectChangePublisher(user: user)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.relationshipUpdatePublisher.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.meObserver = RelationshipViewModel.createObjectChangePublisher(user: me)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.relationshipUpdatePublisher.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RelationshipViewModel {
|
||||||
|
|
||||||
|
public static func createObjectChangePublisher(user: MastodonUser) -> AnyPublisher<Void, Never> {
|
||||||
|
return ManagedObjectObserver
|
||||||
|
.observe(object: user)
|
||||||
|
.map { _ in Void() }
|
||||||
|
.catch { error in
|
||||||
|
return Just(Void())
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RelationshipViewModel {
|
||||||
|
private func update(user: MastodonUser?, me: MastodonUser?) {
|
||||||
|
guard let user = user,
|
||||||
|
let me = me
|
||||||
|
else {
|
||||||
|
reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionSet = RelationshipViewModel.optionSet(user: user, me: me)
|
||||||
|
|
||||||
|
self.isMyself = optionSet.contains(.isMyself)
|
||||||
|
self.isFollowingBy = optionSet.contains(.followingBy)
|
||||||
|
self.isFollowing = optionSet.contains(.following)
|
||||||
|
self.isMuting = optionSet.contains(.muting)
|
||||||
|
self.isBlockingBy = optionSet.contains(.blockingBy)
|
||||||
|
self.isBlocking = optionSet.contains(.blocking)
|
||||||
|
|
||||||
|
|
||||||
|
self.optionSet = optionSet
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reset() {
|
||||||
|
isMyself = false
|
||||||
|
isFollowingBy = false
|
||||||
|
isFollowing = false
|
||||||
|
isMuting = false
|
||||||
|
isBlockingBy = false
|
||||||
|
isBlocking = false
|
||||||
|
optionSet = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RelationshipViewModel {
|
||||||
|
|
||||||
|
public static func optionSet(user: MastodonUser, me: MastodonUser) -> RelationshipActionOptionSet {
|
||||||
|
let isMyself = user.id == me.id && user.domain == me.domain
|
||||||
|
guard !isMyself else {
|
||||||
|
return [.isMyself]
|
||||||
|
}
|
||||||
|
|
||||||
|
let isProtected = user.locked
|
||||||
|
let isFollowingBy = me.followingBy.contains(user)
|
||||||
|
let isFollowing = user.followingBy.contains(me)
|
||||||
|
let isPending = user.followRequestedBy.contains(me)
|
||||||
|
let isMuting = user.mutingBy.contains(me)
|
||||||
|
let isBlockingBy = me.blockingBy.contains(user)
|
||||||
|
let isBlocking = user.blockingBy.contains(me)
|
||||||
|
|
||||||
|
var optionSet: RelationshipActionOptionSet = [.follow]
|
||||||
|
|
||||||
|
if isMyself {
|
||||||
|
optionSet.insert(.isMyself)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isProtected {
|
||||||
|
optionSet.insert(.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFollowingBy {
|
||||||
|
optionSet.insert(.followingBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFollowing {
|
||||||
|
optionSet.insert(.following)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPending {
|
||||||
|
optionSet.insert(.pending)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMuting {
|
||||||
|
optionSet.insert(.muting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBlockingBy {
|
||||||
|
optionSet.insert(.blockingBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBlocking {
|
||||||
|
optionSet.insert(.blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionSet
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue