feat: add hashtag and news list for Discovery scene

This commit is contained in:
CMK 2022-04-13 20:43:16 +08:00
parent af619e198a
commit 080f20d253
58 changed files with 1619 additions and 299 deletions

View File

@ -31,7 +31,6 @@
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; };
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; };
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; };
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; };
@ -223,6 +222,16 @@
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */; };
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; };
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */; };
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */; };
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */; };
DB3E6FE42806A5B800B035AE /* DiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE32806A5B800B035AE /* DiscoverySection.swift */; };
DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE62806A7A200B035AE /* DiscoveryItem.swift */; };
DB3E6FE92806BD2200B035AE /* ThemeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE82806BD2200B035AE /* ThemeService.swift */; };
DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FEB2806D7F100B035AE /* DiscoveryNewsViewController.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 */; };
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */; };
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
@ -376,8 +385,6 @@
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; };
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; };
DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7CA271D5A0300BE3819 /* LineChartView.swift */; };
DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */; };
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */; };
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
@ -508,16 +515,7 @@
DBBC24AA26A5301B00398BB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24A926A5301B00398BB9 /* MastodonSDK */; };
DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */; };
DBBC24B826A5421800398BB9 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24B726A5421800398BB9 /* CommonOSLog */; };
DBBC24BC26A542F500398BB9 /* ThemeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BB26A542F500398BB9 /* ThemeService.swift */; };
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BE26A5443100398BB9 /* MastodonTheme.swift */; };
DBBC24C126A5443100398BB9 /* SystemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BF26A5443100398BB9 /* SystemTheme.swift */; };
DBBC24C426A544B900398BB9 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24C326A544B900398BB9 /* Theme.swift */; };
DBBC24C626A5456000398BB9 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24C326A544B900398BB9 /* Theme.swift */; };
DBBC24C726A5456400398BB9 /* SystemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BF26A5443100398BB9 /* SystemTheme.swift */; };
DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BB26A542F500398BB9 /* ThemeService.swift */; };
DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BE26A5443100398BB9 /* MastodonTheme.swift */; };
DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; };
DBBC24CF26A547AE00398BB9 /* ThemeService+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */; };
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; };
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; };
@ -735,7 +733,6 @@
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = "<group>"; };
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = "<group>"; };
2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = "<group>"; };
2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = "<group>"; };
@ -956,6 +953,16 @@
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = "<group>"; };
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = "<group>"; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewController.swift; sourceTree = "<group>"; };
DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewModel.swift; sourceTree = "<group>"; };
DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryHashtagsViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB3E6FE32806A5B800B035AE /* DiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverySection.swift; sourceTree = "<group>"; };
DB3E6FE62806A7A200B035AE /* DiscoveryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryItem.swift; sourceTree = "<group>"; };
DB3E6FE82806BD2200B035AE /* ThemeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeService.swift; sourceTree = "<group>"; };
DB3E6FEB2806D7F100B035AE /* DiscoveryNewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryNewsViewController.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>"; };
DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryNewsViewModel+State.swift"; sourceTree = "<group>"; };
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>"; };
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@ -1126,8 +1133,6 @@
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = "<group>"; };
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = "<group>"; };
DB71C7CA271D5A0300BE3819 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = "<group>"; };
DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurveAlgorithm.swift; sourceTree = "<group>"; };
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = "<group>"; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
@ -1263,11 +1268,6 @@
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>"; };
DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = "<group>"; };
DBBC24BB26A542F500398BB9 /* ThemeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeService.swift; sourceTree = "<group>"; };
DBBC24BE26A5443100398BB9 /* MastodonTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonTheme.swift; sourceTree = "<group>"; };
DBBC24BF26A5443100398BB9 /* SystemTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemTheme.swift; sourceTree = "<group>"; };
DBBC24C326A544B900398BB9 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeService+Appearance.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>"; };
@ -1668,7 +1668,6 @@
2D5A3D0125CF8640002347D6 /* Vender */ = {
isa = PBXGroup;
children = (
DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */,
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
DB51D170262832380062B7A1 /* BlurHashDecode.swift */,
DB51D171262832380062B7A1 /* BlurHashEncode.swift */,
@ -1687,7 +1686,6 @@
DB45FB0425CA87B4005A8AC7 /* APIService */,
DB49A61925FF327D00B98345 /* EmojiService */,
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
DBBC24BD26A5441A00398BB9 /* ThemeService */,
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
@ -1731,6 +1729,7 @@
DB4F097626A0398000D62E92 /* Compose */,
DB0617F727855B010030EE79 /* Notification */,
DB4F097726A039A200D62E92 /* Search */,
DB3E6FE52806A5BA00B035AE /* Discovery */,
DB0617FA27855B660030EE79 /* Settings */,
DBCBED2226132E1D00B49291 /* FetchedResultsController */,
);
@ -1817,7 +1816,6 @@
isa = PBXGroup;
children = (
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */,
DB71C7CA271D5A0300BE3819 /* LineChartView.swift */,
);
path = View;
sourceTree = "<group>";
@ -2111,6 +2109,44 @@
path = Resources;
sourceTree = "<group>";
};
DB3E6FDE2806A41200B035AE /* Hashtags */ = {
isa = PBXGroup;
children = (
DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */,
DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */,
DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */,
);
path = Hashtags;
sourceTree = "<group>";
};
DB3E6FE52806A5BA00B035AE /* Discovery */ = {
isa = PBXGroup;
children = (
DB3E6FE32806A5B800B035AE /* DiscoverySection.swift */,
DB3E6FE62806A7A200B035AE /* DiscoveryItem.swift */,
);
path = Discovery;
sourceTree = "<group>";
};
DB3E6FEA2806BD2500B035AE /* MastodonUI */ = {
isa = PBXGroup;
children = (
DB3E6FE82806BD2200B035AE /* ThemeService.swift */,
);
path = MastodonUI;
sourceTree = "<group>";
};
DB3E6FED2806D7FC00B035AE /* News */ = {
isa = PBXGroup;
children = (
DB3E6FEB2806D7F100B035AE /* DiscoveryNewsViewController.swift */,
DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */,
DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */,
DB3E6FF22806D97400B035AE /* DiscoveryNewsViewModel+State.swift */,
);
path = News;
sourceTree = "<group>";
};
DB427DC925BAA00100D1B89D = {
isa = PBXGroup;
children = (
@ -2706,6 +2742,7 @@
isa = PBXGroup;
children = (
DB084B5125CBC56300F898ED /* CoreDataStack */,
DB3E6FEA2806BD2500B035AE /* MastodonUI */,
DB6C8C0525F0921200AAA452 /* MastodonSDK */,
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
@ -2723,7 +2760,6 @@
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */,
0FAA101B25E10E760017CCDE /* UIFont.swift */,
2D206B9125F60EA700143C56 /* UIControl.swift */,
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */,
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
@ -3020,18 +3056,6 @@
path = Service;
sourceTree = "<group>";
};
DBBC24BD26A5441A00398BB9 /* ThemeService */ = {
isa = PBXGroup;
children = (
DBBC24C326A544B900398BB9 /* Theme.swift */,
DBBC24BE26A5443100398BB9 /* MastodonTheme.swift */,
DBBC24BF26A5443100398BB9 /* SystemTheme.swift */,
DBBC24BB26A542F500398BB9 /* ThemeService.swift */,
DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */,
);
path = ThemeService;
sourceTree = "<group>";
};
DBBC24D526A54BCB00398BB9 /* Helper */ = {
isa = PBXGroup;
children = (
@ -3087,6 +3111,8 @@
isa = PBXGroup;
children = (
DBDFF19828055A0900557A48 /* Posts */,
DB3E6FDE2806A41200B035AE /* Hashtags */,
DB3E6FED2806D7FC00B035AE /* News */,
DBDFF19928055A1400557A48 /* DiscoveryViewController.swift */,
DBDFF19B28055BD600557A48 /* DiscoveryViewModel.swift */,
);
@ -3861,6 +3887,7 @@
DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */,
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */,
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */,
@ -3924,6 +3951,7 @@
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */,
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */,
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */,
DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */,
DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */,
DB63F76B279A5ED300455B82 /* NotificationTimelineViewModel+LoadOldestState.swift in Sources */,
@ -3952,6 +3980,7 @@
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */,
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */,
DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */,
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
@ -3993,7 +4022,6 @@
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
DBBC24C126A5443100398BB9 /* SystemTheme.swift in Sources */,
DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */,
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
@ -4020,6 +4048,7 @@
DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */,
DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */,
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
DB3E6FE92806BD2200B035AE /* ThemeService.swift in Sources */,
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */,
DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */,
@ -4056,6 +4085,7 @@
DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */,
2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */,
DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */,
DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */,
DB647C5926F1EA2700F7F82C /* WizardPreference.swift in Sources */,
@ -4078,6 +4108,7 @@
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */,
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */,
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
@ -4139,7 +4170,7 @@
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */,
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */,
DBBC24C426A544B900398BB9 /* Theme.swift in Sources */,
DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */,
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */,
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
@ -4158,7 +4189,6 @@
DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */,
DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */,
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
DBBC24BC26A542F500398BB9 /* ThemeService.swift in Sources */,
DB336F38278D7AAF0031E64B /* Poll+Property.swift in Sources */,
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
@ -4171,7 +4201,6 @@
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */,
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
@ -4179,7 +4208,6 @@
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */,
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */,
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
@ -4201,6 +4229,7 @@
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */,
DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */,
DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */,
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */,
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */,
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
@ -4247,7 +4276,6 @@
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */,
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */,
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */,
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */,
@ -4264,25 +4292,25 @@
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
DB697DDD278F521D004EF2F7 /* DataSourceFacade.swift in Sources */,
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */,
DB3E6FE42806A5B800B035AE /* DiscoverySection.swift in Sources */,
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
DB697DDB278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift in Sources */,
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */,
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */,
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */,
DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */,
DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */,
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */,
DBBC24CF26A547AE00398BB9 /* ThemeService+Appearance.swift in Sources */,
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
@ -4390,15 +4418,11 @@
DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */,
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */,
DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */,
DBBC24C726A5456400398BB9 /* SystemTheme.swift in Sources */,
DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */,
DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */,
DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */,
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */,
DB6746E8278ED639008A6B94 /* MastodonAuthenticationBox.swift in Sources */,
DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */,
DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */,
DBBC24C626A5456000398BB9 /* Theme.swift in Sources */,
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */,
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */,
DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */,

View File

@ -73,6 +73,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@ -89,6 +90,13 @@
ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
<AdditionalOption
key = "NSZombieEnabled"
value = "YES"
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -124,12 +124,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>24</integer>
<integer>22</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>22</integer>
<integer>24</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -0,0 +1,15 @@
//
// DiscoveryItem.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import Foundation
import MastodonSDK
enum DiscoveryItem: Hashable {
case hashtag(Mastodon.Entity.Tag)
case link(Mastodon.Entity.Link)
case bottomLoader
}

View File

@ -0,0 +1,52 @@
//
// DiscoverySection.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import os.log
import UIKit
import MastodonUI
enum DiscoverySection: CaseIterable {
// case posts
case hashtags
case news
case forYou
}
extension DiscoverySection {
static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
struct Configuration { }
static func diffableDataSource(
tableView: UITableView,
context: AppContext,
configuration: Configuration
) -> UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem> {
tableView.register(TrendTableViewCell.self, forCellReuseIdentifier: String(describing: TrendTableViewCell.self))
tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: String(describing: NewsTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
switch item {
case .hashtag(let tag):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TrendTableViewCell.self), for: indexPath) as! TrendTableViewCell
cell.trendView.configure(tag: tag)
return cell
case .link(let link):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NewsTableViewCell.self), for: indexPath) as! NewsTableViewCell
cell.newsView.configure(link: link)
return cell
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
cell.activityIndicatorView.startAnimating()
return cell
}
}
}
}

View File

@ -21,26 +21,7 @@ extension SearchSection {
) -> UICollectionViewDiffableDataSource<SearchSection, SearchItem> {
let trendCellRegister = UICollectionView.CellRegistration<TrendCollectionViewCell, Mastodon.Entity.Tag> { cell, indexPath, item in
let primaryLabelText = "#" + item.name
let secondaryLabelText = L10n.Scene.Search.Recommend.HashTag.peopleTalking(item.talkingPeopleCount ?? 0)
cell.primaryLabel.text = primaryLabelText
cell.secondaryLabel.text = secondaryLabelText
cell.lineChartView.data = (item.history ?? [])
.sorted(by: { $0.day < $1.day }) // latest last
.map { entry in
guard let point = Int(entry.accounts) else {
return .zero
}
return CGFloat(point)
}
cell.isAccessibilityElement = true
cell.accessibilityLabel = [
primaryLabelText,
secondaryLabelText
].joined(separator: ", ")
}
let dataSource = UICollectionViewDiffableDataSource<SearchSection, SearchItem>(

View File

@ -17,14 +17,3 @@ extension Mastodon.Entity.Tag: Hashable {
}
}
extension Mastodon.Entity.Tag {
/// the sum of recent 2 days
public var talkingPeopleCount: Int? {
return history?
.prefix(2)
.compactMap { Int($0.accounts) }
.reduce(0, +)
}
}

View File

@ -1,11 +1,13 @@
//
// ThemeService+Appearance.swift
// ThemeService.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-19.
// Created by MainasuK on 2022-4-13.
//
import UIKit
import MastodonCommon
import MastodonUI
extension ThemeService {
func set(themeName: ThemeName) {

View File

@ -1,70 +0,0 @@
//
// UIView.swift
// Mastodon
//
// Created by sxiaojian on 2021/2/4.
//
import UIKit
// MARK: - Convenience view creation method
extension UIView {
static let separatorColor: UIColor = {
UIColor(dynamicProvider: { collection in
switch collection.userInterfaceStyle {
case .dark:
return ThemeService.shared.currentTheme.value.separator
default:
return .separator
}
})
}()
static var separatorLine: UIView {
let line = UIView()
line.backgroundColor = UIView.separatorColor
return line
}
static func separatorLineHeight(of view: UIView) -> CGFloat {
return 1.0 / view.traitCollection.displayScale
}
}
// MARK: - Convenience view appearance modification method
extension UIView {
@discardableResult
func applyCornerRadius(radius: CGFloat) -> Self {
layer.masksToBounds = true
layer.cornerRadius = radius
layer.cornerCurve = .continuous
return self
}
@discardableResult
func applyShadow(
color: UIColor,
alpha: Float,
x: CGFloat,
y: CGFloat,
blur: CGFloat,
spread: CGFloat = 0) -> Self
{
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowOpacity = alpha
layer.shadowOffset = CGSize(width: x, height: y)
layer.shadowRadius = blur / 2.0
if spread == 0 {
layer.shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
return self
}
}

View File

@ -5,17 +5,3 @@
// Created by MainasuK Cirno on 2021-7-5.
//
import UIKit
import MastodonExtension
extension UserDefaults {
@objc dynamic var currentThemeNameRawValue: String {
get {
register(defaults: [#function: ThemeName.mastodon.rawValue])
return string(forKey: #function) ?? ThemeName.mastodon.rawValue
}
set { self[#function] = newValue }
}
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import MastodonAsset
import MastodonLocalization
import MastodonUI
protocol ComposeStatusPollOptionCollectionViewCellDelegate: AnyObject {
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField)

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import Tabman
import MastodonAsset
import MastodonUI
public class DiscoveryViewController: TabmanViewController, NeedsDependency {

View File

@ -13,7 +13,9 @@ final class DiscoveryViewModel {
// input
let context: AppContext
let discoveryViewController: DiscoveryPostsViewController
let discoveryPostsViewController: DiscoveryPostsViewController
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
let discoveryNewsViewController: DiscoveryNewsViewController
// output
let barItems: [TMBarItemable] = {
@ -28,19 +30,37 @@ final class DiscoveryViewModel {
var viewControllers: [ScrollViewContainer] {
return [
discoveryViewController,
discoveryPostsViewController,
discoveryHashtagsViewController,
discoveryNewsViewController,
]
}
init(context: AppContext, coordinator: SceneCoordinator) {
func setupDependency(_ needsDependency: NeedsDependency) {
needsDependency.context = context
needsDependency.coordinator = coordinator
}
self.context = context
discoveryViewController = {
discoveryPostsViewController = {
let viewController = DiscoveryPostsViewController()
viewController.context = context
viewController.coordinator = coordinator
setupDependency(viewController)
viewController.viewModel = DiscoveryPostsViewModel(context: context)
return viewController
}()
discoveryHashtagsViewController = {
let viewController = DiscoveryHashtagsViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryHashtagsViewModel(context: context)
return viewController
}()
discoveryNewsViewController = {
let viewController = DiscoveryNewsViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryNewsViewModel(context: context)
return viewController
}()
// end init
}

View File

@ -0,0 +1,113 @@
//
// DiscoveryHashtagsViewController.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import os.log
import UIKit
import Combine
import MastodonUI
final class DiscoveryHashtagsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
let logger = Logger(subsystem: "TrendPostsViewController", category: "ViewController")
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: DiscoveryHashtagsViewModel!
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
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension DiscoveryHashtagsViewController {
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
)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.viewDidAppeared.send()
}
}
// MARK: - UITableViewDelegate
extension DiscoveryHashtagsViewController: 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 .hashtag(tag) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name)
coordinator.present(
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
from: self,
transition: .show
)
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? TrendTableViewCell else { return }
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
if let lastItem = diffableDataSource.snapshot().itemIdentifiers.last, item == lastItem {
cell.configureSeparator(style: .edge)
}
}
}
// MARK: ScrollViewContainer
extension DiscoveryHashtagsViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
}

View File

@ -0,0 +1,42 @@
//
// DiscoveryHashtagsViewModel+Diffable.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
extension DiscoveryHashtagsViewModel {
func setupDiffableDataSource(
tableView: UITableView
) {
diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView,
context: context,
configuration: DiscoverySection.Configuration()
)
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()
snapshot.appendSections([.hashtags])
diffableDataSource?.apply(snapshot)
$hashtags
.receive(on: DispatchQueue.main)
.sink { [weak self] hashtags in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()
snapshot.appendSections([.hashtags])
let items = hashtags.map { DiscoveryItem.hashtag($0) }
snapshot.appendItems(items, toSection: .hashtags)
diffableDataSource.apply(snapshot)
}
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,63 @@
//
// DiscoveryHashtagsViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import os.log
import UIKit
import Combine
import GameplayKit
import CoreData
import CoreDataStack
import MastodonSDK
final class DiscoveryHashtagsViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let viewDidAppeared = PassthroughSubject<Void, Never>()
// output
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
@Published var hashtags: [Mastodon.Entity.Tag] = []
init(context: AppContext) {
self.context = context
// end init
Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared
)
.compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in
return authenticationBox
}
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
.asyncMap { authenticationBox in
try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
}
.retry(3)
.map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.hashtags = response.value.filter { !$0.name.isEmpty }
case .failure:
break
}
}
.store(in: &disposeBag)
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}

View File

@ -0,0 +1,133 @@
//
// DiscoveryNewsViewController.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import os.log
import UIKit
import Combine
import MastodonUI
final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
let logger = Logger(subsystem: "TrendPostsViewController", category: "ViewController")
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: DiscoveryNewsViewModel!
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 DiscoveryNewsViewController {
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(DiscoveryNewsViewController.refreshControlValueChanged(_:)), for: .valueChanged)
viewModel.didLoadLatest
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.refreshControl.endRefreshing()
}
.store(in: &disposeBag)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Loading.self)
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refreshControl.endRefreshing()
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
}
extension DiscoveryNewsViewController {
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
guard viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Reloading.self) else {
sender.endRefreshing()
return
}
}
}
// MARK: - UITableViewDelegate
extension DiscoveryNewsViewController: 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 .link(link) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
guard let url = URL(string: link.url) else { return }
coordinator.present(
scene: .safari(url: url),
from: self,
transition: .safariPresent(animated: true, completion: nil)
)
}
}
// MARK: ScrollViewContainer
extension DiscoveryNewsViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
}

View File

@ -0,0 +1,60 @@
//
// DiscoveryNewsViewModel+Diffable.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
import Combine
extension DiscoveryNewsViewModel {
func setupDiffableDataSource(
tableView: UITableView
) {
diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView,
context: context,
configuration: DiscoverySection.Configuration()
)
stateMachine.enter(State.Reloading.self)
$links
.receive(on: DispatchQueue.main)
.sink { [weak self] links in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()
snapshot.appendSections([.news])
let items = links.map { DiscoveryItem.link($0) }
snapshot.appendItems(items, toSection: .news)
if let currentState = self.stateMachine.currentState {
switch currentState {
case is State.Initial,
is State.Loading,
is State.Idle,
is State.Fail:
if !items.isEmpty {
snapshot.appendItems([.bottomLoader], toSection: .news)
}
case is State.Reloading:
break
case is State.NoMore:
break
default:
assertionFailure()
break
}
}
diffableDataSource.applySnapshot(snapshot, animated: false)
}
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,209 @@
//
// DiscoveryNewsViewModel+State.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import os.log
import Foundation
import GameplayKit
import MastodonSDK
extension DiscoveryNewsViewModel {
class State: GKState, NamingState {
let logger = Logger(subsystem: "DiscoveryNewsViewModel.State", category: "StateMachine")
let id = UUID()
var name: String {
String(describing: Self.self)
}
weak var viewModel: DiscoveryNewsViewModel?
init(viewModel: DiscoveryNewsViewModel) {
self.viewModel = viewModel
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
let previousState = previousState as? DiscoveryNewsViewModel.State
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<nil>")")
}
@MainActor
func enter(state: State.Type) {
stateMachine?.enter(state)
}
deinit {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)")
}
}
}
extension DiscoveryNewsViewModel.State {
class Initial: DiscoveryNewsViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Reloading.Type:
return true
default:
return false
}
}
}
class Reloading: DiscoveryNewsViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Loading.Type:
return true
default:
return false
}
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
viewModel.links = []
stateMachine.enter(Loading.self)
}
}
class Fail: DiscoveryNewsViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Loading.Type:
return true
default:
return false
}
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let _ = viewModel, let stateMachine = stateMachine else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function)
stateMachine.enter(Loading.self)
}
}
}
class Idle: DiscoveryNewsViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Reloading.Type, is Loading.Type:
return true
default:
return false
}
}
}
class Loading: DiscoveryNewsViewModel.State {
var offset: Int?
var isReloading: Bool { return offset == nil }
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Fail.Type:
return true
case is Idle.Type:
return true
case is NoMore.Type:
return true
default:
return false
}
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
switch previousState {
case is Reloading:
offset = nil
default:
break
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
let offset = self.offset
Task {
do {
let response = try await viewModel.context.apiService.trendLinks(
domain: authenticationBox.domain,
query: Mastodon.API.Trends.StatusQuery(
offset: offset,
limit: nil
)
)
let newOffset: Int? = {
guard let offset = response.link?.offset else { return nil }
return self.offset.flatMap { max($0, offset) } ?? offset
}()
let hasMore: Bool = {
guard let newOffset = newOffset else { return false }
return newOffset != self.offset // not the same one
}()
self.offset = newOffset
var hasNewItemsAppend = false
var links = viewModel.links
for link in response.value {
guard !links.contains(link) else { continue }
links.append(link)
hasNewItemsAppend = true
}
if hasNewItemsAppend, hasMore {
await enter(state: Idle.self)
} else {
await enter(state: NoMore.self)
}
viewModel.links = links
viewModel.didLoadLatest.send()
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch news fail: \(error.localizedDescription)")
await enter(state: Fail.self)
viewModel.didLoadLatest.send()
}
} // end Task
} // end func
}
class NoMore: DiscoveryNewsViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Reloading.Type:
return true
default:
return false
}
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
}
}
}

View File

@ -0,0 +1,51 @@
//
// DiscoveryNewsViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-4-13.
//
import os.log
import UIKit
import Combine
import GameplayKit
import CoreData
import CoreDataStack
import MastodonSDK
final class DiscoveryNewsViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@Published var links: [Mastodon.Entity.Link] = []
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
private(set) lazy var stateMachine: GKStateMachine = {
let stateMachine = GKStateMachine(states: [
State.Initial(viewModel: self),
State.Reloading(viewModel: self),
State.Fail(viewModel: self),
State.Idle(viewModel: self),
State.Loading(viewModel: self),
State.NoMore(viewModel: self),
])
stateMachine.enter(State.Initial.self)
return stateMachine
}()
let didLoadLatest = PassthroughSubject<Void, Never>()
init(context: AppContext) {
self.context = context
// end init
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}

View File

@ -30,6 +30,8 @@ final class DiscoveryPostsViewController: UIViewController, NeedsDependency, Med
return tableView
}()
let refreshControl = UIRefreshControl()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
@ -65,6 +67,16 @@ extension DiscoveryPostsViewController {
statusTableViewCellDelegate: self
)
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(DiscoveryPostsViewController.refreshControlValueChanged(_:)), for: .valueChanged)
viewModel.didLoadLatest
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.refreshControl.endRefreshing()
}
.store(in: &disposeBag)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
@ -77,6 +89,24 @@ extension DiscoveryPostsViewController {
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refreshControl.endRefreshing()
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
}
extension DiscoveryPostsViewController {
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
guard viewModel.stateMachine.enter(DiscoveryPostsViewModel.State.Reloading.self) else {
sender.endRefreshing()
return
}
}
}
// MARK: - UITableViewDelegate

View File

@ -28,7 +28,7 @@ extension DiscoveryPostsViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
@ -46,7 +46,9 @@ extension DiscoveryPostsViewModel {
is State.Loading,
is State.Idle,
is State.Fail:
snapshot.appendItems([.bottomLoader], toSection: .main)
if !items.isEmpty {
snapshot.appendItems([.bottomLoader], toSection: .main)
}
case is State.NoMore:
break
default:
@ -54,7 +56,7 @@ extension DiscoveryPostsViewModel {
break
}
}
diffableDataSource.applySnapshot(snapshot, animated: false)
}
.store(in: &disposeBag)

View File

@ -13,7 +13,7 @@ import MastodonSDK
extension DiscoveryPostsViewModel {
class State: GKState, NamingState {
let logger = Logger(subsystem: "TrendPostsViewModel.State", category: "StateMachine")
let logger = Logger(subsystem: "DiscoveryPostsViewModel.State", category: "StateMachine")
let id = UUID()
@ -132,7 +132,6 @@ extension DiscoveryPostsViewModel.State {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
switch previousState {
case is Reloading:
offset = nil
@ -182,10 +181,11 @@ extension DiscoveryPostsViewModel.State {
await enter(state: NoMore.self)
}
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
viewModel.didLoadLatest.send()
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user timeline fail: \(error.localizedDescription)")
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch posts fail: \(error.localizedDescription)")
await enter(state: Fail.self)
viewModel.didLoadLatest.send()
}
} // end Task
} // end func

View File

@ -37,6 +37,8 @@ final class DiscoveryPostsViewModel {
return stateMachine
}()
let didLoadLatest = PassthroughSubject<Void, Never>()
init(context: AppContext) {
self.context = context
self.statusFetchedResultsController = StatusFetchedResultsController(

View File

@ -17,6 +17,7 @@ import AlamofireImage
import StoreKit
import MastodonAsset
import MastodonLocalization
import MastodonUI
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -291,7 +292,7 @@ extension HomeTimelineViewController {
tableView.deselectRow(with: transitionCoordinator, animated: animated)
// needs trigger manually after onboarding dismiss
setNeedsStatusBarAppearanceUpdate()
setNeedsStatusBarAppearanceUpdate()
}
override func viewDidAppear(_ animated: Bool) {

View File

@ -11,6 +11,7 @@ import Combine
import SafariServices
import MastodonAsset
import MastodonLocalization
import MastodonUI
class MainTabBarController: UITabBarController {

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import Combine
import CoreDataStack
import MastodonUI
protocol SidebarViewControllerDelegate: AnyObject {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)

View File

@ -9,45 +9,13 @@ import UIKit
import Combine
import MetaTextKit
import MastodonAsset
import MastodonUI
final class TrendCollectionViewCell: UICollectionViewCell {
var _disposeBag = Set<AnyCancellable>()
let container: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 16
return stackView
}()
let infoContainer: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
}()
let lineChartContainer: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
}()
let primaryLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
label.textColor = Asset.Colors.Label.primary.color
return label
}()
let secondaryLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
label.textColor = Asset.Colors.Label.secondary.color
return label
}()
let lineChartView = LineChartView()
let trendView = TrendView()
override func prepareForReuse() {
super.prepareForReuse()
@ -77,44 +45,13 @@ extension TrendCollectionViewCell {
}
.store(in: &_disposeBag)
container.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(container)
trendView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(trendView)
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
container.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11),
])
container.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
container.isLayoutMarginsRelativeArrangement = true
// container: H - [ info container | padding | line chart container ]
container.addArrangedSubview(infoContainer)
// info container: V - [ primary | secondary ]
infoContainer.addArrangedSubview(primaryLabel)
infoContainer.addArrangedSubview(secondaryLabel)
// padding
let padding = UIView()
container.addArrangedSubview(padding)
// line chart
container.addArrangedSubview(lineChartContainer)
let lineChartViewTopPadding = UIView()
let lineChartViewBottomPadding = UIView()
lineChartViewTopPadding.translatesAutoresizingMaskIntoConstraints = false
lineChartViewBottomPadding.translatesAutoresizingMaskIntoConstraints = false
lineChartView.translatesAutoresizingMaskIntoConstraints = false
lineChartContainer.addArrangedSubview(lineChartViewTopPadding)
lineChartContainer.addArrangedSubview(lineChartView)
lineChartContainer.addArrangedSubview(lineChartViewBottomPadding)
NSLayoutConstraint.activate([
lineChartView.widthAnchor.constraint(equalToConstant: 50),
lineChartView.heightAnchor.constraint(equalToConstant: 26),
lineChartViewTopPadding.heightAnchor.constraint(equalTo: lineChartViewBottomPadding.heightAnchor),
trendView.topAnchor.constraint(equalTo: contentView.topAnchor),
trendView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
trendView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
trendView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import MastodonAsset
import MastodonLocalization
import MastodonUI
protocol SearchHistoryTableHeaderViewDelegate: AnyObject {
func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton)

View File

@ -11,6 +11,7 @@ import Combine
import UIKit
import MastodonAsset
import MastodonLocalization
import MastodonUI
protocol ContentWarningOverlayViewDelegate: AnyObject {
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView)

View File

@ -58,7 +58,7 @@ extension MediaView {
}()
if let previewURL = configuration.previewURL,
let url = URL(string: previewURL)
let url = URL(string: previewURL)
{
let placeholder = UIImage.placeholder(color: .systemGray6)
let request = URLRequest(url: url)

View File

@ -6,6 +6,7 @@
//
import UIKit
import MastodonUI
final class ThreadMetaView: UIView {

View File

@ -53,4 +53,17 @@ extension APIService {
return response
}
func trendLinks(
domain: String,
query: Mastodon.API.Trends.LinkQuery
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Link]> {
let response = try await Mastodon.API.Trends.links(
session: session,
domain: domain,
query: query
).singleOutput()
return response
}
}

View File

@ -12,6 +12,7 @@ import CoreDataStack
import MastodonSDK
import MastodonAsset
import MastodonLocalization
import MastodonCommon
final class SettingService {
@ -190,18 +191,6 @@ extension SettingService {
extension SettingService {
static func updatePreference(setting: Setting) {
// set appearance
// let userInterfaceStyle: UIUserInterfaceStyle = {
// switch setting.appearance {
// case .automatic: return .unspecified
// case .light: return .light
// case .dark: return .dark
// }
// }()
// if UserDefaults.shared.customUserInterfaceStyle != userInterfaceStyle {
// UserDefaults.shared.customUserInterfaceStyle = userInterfaceStyle
// }
// set theme
let themeName: ThemeName = setting.preferredTrueBlackDarkMode ? .system : .mastodon
if UserDefaults.shared.currentThemeNameRawValue != themeName.rawValue {
@ -223,6 +212,6 @@ extension SettingService {
if UserDefaults.shared.preferredUsingDefaultBrowser != setting.preferredUsingDefaultBrowser {
UserDefaults.shared.preferredUsingDefaultBrowser = setting.preferredUsingDefaultBrowser
}
}
}

View File

@ -0,0 +1,26 @@
//
// Preference+Theme.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
import MastodonExtension
public enum ThemeName: String, CaseIterable {
case system
case mastodon
}
extension UserDefaults {
@objc public dynamic var currentThemeNameRawValue: String {
get {
register(defaults: [#function: ThemeName.mastodon.rawValue])
return string(forKey: #function) ?? ThemeName.mastodon.rawValue
}
set { self[#function] = newValue }
}
}

View File

@ -12,3 +12,37 @@ extension UIView {
return UIScreen.main.scale != UIScreen.main.nativeScale
}
}
extension UIView {
@discardableResult
public func applyCornerRadius(radius: CGFloat) -> Self {
layer.masksToBounds = true
layer.cornerRadius = radius
layer.cornerCurve = .continuous
return self
}
@discardableResult
public func applyShadow(
color: UIColor,
alpha: Float,
x: CGFloat,
y: CGFloat,
blur: CGFloat,
spread: CGFloat = 0
) -> Self {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowOpacity = alpha
layer.shadowOffset = CGSize(width: x, height: y)
layer.shadowRadius = blur / 2.0
if spread == 0 {
layer.shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
return self
}
}

View File

@ -0,0 +1,12 @@
//
// UInt64.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import Foundation
extension UInt64 {
public static let second: UInt64 = 1_000_000_000
}

View File

@ -71,9 +71,9 @@ extension Mastodon.API.Trends {
.appendingPathComponent("statuses")
}
/// Trending tags
/// Trending status
///
/// Tags that are being used more frequently within the past week.
/// TBD
///
/// Version history:
/// 3.?.?
@ -83,7 +83,7 @@ extension Mastodon.API.Trends {
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - query: query
/// - Returns: `AnyPublisher` contains `Hashtags` nested in the response
/// - Returns: `[Status]` nested in the response
public static func statuses(
session: URLSession,
@ -126,3 +126,47 @@ extension Mastodon.API.Trends {
}
}
extension Mastodon.API.Trends {
static func trendLinksURL(domain: String) -> URL {
Mastodon.API.endpointURL(domain: domain)
.appendingPathComponent("trends")
.appendingPathComponent("links")
}
/// Trending links
///
/// TBD
///
/// Version history:
/// 3.?.?
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/instance/trends/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - query: query
/// - Returns: `[Link]` nested in the response
public static func links(
session: URLSession,
domain: String,
query: Mastodon.API.Trends.LinkQuery?
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Link]>, Error> {
let request = Mastodon.API.get(
url: trendLinksURL(domain: domain),
query: query,
authorization: nil
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Link].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
public typealias LinkQuery = StatusQuery
}

View File

@ -0,0 +1,54 @@
//
// Mastodon+Entity+Link.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import Foundation
extension Mastodon.Entity {
/// History
///
/// - Since: 3.5.0
/// - Version: 3.5.1
/// # Last Update
/// 2022/4/13
/// # Reference
/// [Document](TBD)
public struct Link: Codable {
public let url: String
public let title: String
public let description: String
public let providerName: String
public let providerURL: String
public let image: String
public let width: Int
public let height: Int
public let blurhash: String
public let history: [History]
enum CodingKeys: String, CodingKey {
case url
case title
case description
case providerName = "provider_name"
case providerURL = "provider_url"
case image
case width
case height
case blurhash
case history
}
}
}
extension Mastodon.Entity.Link: Hashable {
public static func == (lhs: Mastodon.Entity.Link, rhs: Mastodon.Entity.Link) -> Bool {
return lhs.url == rhs.url
}
public func hash(into hasher: inout Hasher) {
hasher.combine(url)
}
}

View File

@ -37,6 +37,7 @@ extension Mastodon.Entity {
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(url)
}
}
}

View File

@ -0,0 +1,21 @@
//
// Mastodon+Entity+Link.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import Foundation
import MastodonSDK
extension Mastodon.Entity.Link {
/// the sum of recent 2 days
public var talkingPeopleCount: Int? {
return history
.prefix(2)
.compactMap { Int($0.accounts) }
.reduce(0, +)
}
}

View File

@ -0,0 +1,21 @@
//
// Mastodon+Entity+Tag.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import Foundation
import MastodonSDK
extension Mastodon.Entity.Tag {
/// the sum of recent 2 days
public var talkingPeopleCount: Int? {
return history?
.prefix(2)
.compactMap { Int($0.accounts) }
.reduce(0, +)
}
}

View File

@ -0,0 +1,34 @@
//
// UIView.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
extension UIView {
static let separatorColor: UIColor = {
UIColor(dynamicProvider: { collection in
switch collection.userInterfaceStyle {
case .dark:
return ThemeService.shared.currentTheme.value.separator
default:
return .separator
}
})
}()
public static var separatorLine: UIView {
let line = UIView()
line.backgroundColor = UIView.separatorColor
return line
}
public static func separatorLineHeight(of view: UIView) -> CGFloat {
return 1.0 / view.traitCollection.displayScale
}
}

View File

@ -7,6 +7,7 @@
import UIKit
import MastodonAsset
import MastodonCommon
struct MastodonTheme: Theme {

View File

@ -7,6 +7,7 @@
import UIKit
import MastodonAsset
import MastodonCommon
struct SystemTheme: Theme {

View File

@ -6,6 +6,7 @@
//
import UIKit
import MastodonCommon
public protocol Theme {
@ -42,17 +43,3 @@ public protocol Theme {
var notificationStatusBorderColor: UIColor { get }
}
public enum ThemeName: String, CaseIterable {
case system
case mastodon
}
extension ThemeName {
public var theme: Theme {
switch self {
case .system: return SystemTheme()
case .mastodon: return MastodonTheme()
}
}
}

View File

@ -8,16 +8,17 @@
import UIKit
import Combine
import AppShared
import MastodonCommon
// ref: https://zamzam.io/protocol-oriented-themes-for-ios-apps/
final class ThemeService {
public final class ThemeService {
static let tintColor: UIColor = .label
public static let tintColor: UIColor = .label
// MARK: - Singleton
public static let shared = ThemeService()
let currentTheme: CurrentValueSubject<Theme, Never>
public let currentTheme: CurrentValueSubject<Theme, Never>
private init() {
let theme = ThemeName(rawValue: UserDefaults.shared.currentThemeNameRawValue)?.theme ?? ThemeName.mastodon.theme
@ -25,3 +26,12 @@ final class ThemeService {
}
}
extension ThemeName {
public var theme: Theme {
switch self {
case .system: return SystemTheme()
case .mastodon: return MastodonTheme()
}
}
}

View File

@ -0,0 +1,43 @@
//
// NewsView+Configuration.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
import MastodonSDK
import MastodonLocalization
import AlamofireImage
extension NewsView {
public func configure(link: Mastodon.Entity.Link) {
providerNameLabel.text = link.providerName
headlineLabel.text = link.title
footnoteLabel.text = L10n.Scene.Search.Recommend.HashTag.peopleTalking(link.talkingPeopleCount ?? 0)
let configuration = MediaView.Configuration(
info: .image(info: .init(
aspectRadio: CGSize(width: link.width, height: link.height),
assetURL: link.image
)),
blurhash: link.blurhash
)
imageView.setup(configuration: configuration)
if let previewURL = configuration.previewURL,
let url = URL(string: previewURL)
{
let placeholder = UIImage.placeholder(color: .systemGray6)
let request = URLRequest(url: url)
ImageDownloader.default.download(request, completion: { response in
switch response.result {
case .success(let image):
configuration.previewImage = image
case .failure:
configuration.previewImage = placeholder
}
})
}
} // end func
}

View File

@ -0,0 +1,102 @@
//
// NewsView.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
import MastodonAsset
public final class NewsView: UIView {
let container = UIStackView()
let providerNameLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .semibold))
label.textColor = Asset.Colors.Label.primary.color
return label
}()
let headlineLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
label.textColor = Asset.Colors.Label.primary.color
label.numberOfLines = 0
return label
}()
let footnoteLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .medium))
label.textColor = Asset.Colors.Label.secondary.color
return label
}()
let imageView = MediaView()
public func prepareForReuse() {
imageView.prepareForReuse()
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension NewsView {
private func _init() {
// container: H - [ textContainer | imageView ]
container.axis = .horizontal
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),
])
// textContainer: V - [ providerContainer | headlineLabel | (spacer) | footnoteLabel ]
let textContainer = UIStackView()
textContainer.axis = .vertical
textContainer.spacing = 4
container.addArrangedSubview(textContainer)
// providerContainer: H - [ providerFavIconImageView | providerNameLabel | (spacer) ]
let providerContainer = UIStackView()
providerContainer.axis = .horizontal
textContainer.addArrangedSubview(providerContainer)
providerContainer.addArrangedSubview(providerNameLabel)
// headlineLabel
textContainer.addArrangedSubview(headlineLabel)
let spacer = UIView()
spacer.translatesAutoresizingMaskIntoConstraints = false
textContainer.addArrangedSubview(spacer)
NSLayoutConstraint.activate([
spacer.heightAnchor.constraint(equalToConstant: 24).priority(.required - 1),
])
// footnoteLabel
textContainer.addArrangedSubview(footnoteLabel)
// imageView
imageView.translatesAutoresizingMaskIntoConstraints = false
container.addArrangedSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 132).priority(.required - 1),
])
imageView.isUserInteractionEnabled = false
}
}

View File

@ -0,0 +1,35 @@
//
// TrendView+Configuration.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
import MastodonSDK
import MastodonLocalization
extension TrendView {
public func configure(tag: Mastodon.Entity.Tag) {
let primaryLabelText = "#" + tag.name
let secondaryLabelText = L10n.Scene.Search.Recommend.HashTag.peopleTalking(tag.talkingPeopleCount ?? 0)
primaryLabel.text = primaryLabelText
secondaryLabel.text = secondaryLabelText
lineChartView.data = (tag.history ?? [])
.sorted(by: { $0.day < $1.day }) // latest last
.map { entry in
guard let point = Int(entry.accounts) else {
return .zero
}
return CGFloat(point)
}
isAccessibilityElement = true
accessibilityLabel = [
primaryLabelText,
secondaryLabelText
].joined(separator: ", ")
}
}

View File

@ -0,0 +1,100 @@
//
// TrendView.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
import MastodonAsset
public final class TrendView: UIView {
let container: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 16
return stackView
}()
let infoContainer: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
}()
let lineChartContainer: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
}()
let primaryLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
label.textColor = Asset.Colors.Label.primary.color
return label
}()
let secondaryLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
label.textColor = Asset.Colors.Label.secondary.color
return label
}()
let lineChartView = LineChartView()
public override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension TrendView {
private func _init() {
container.translatesAutoresizingMaskIntoConstraints = false
addSubview(container)
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: topAnchor, constant: 11),
container.leadingAnchor.constraint(equalTo: leadingAnchor),
container.trailingAnchor.constraint(equalTo: trailingAnchor),
bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11),
])
// container: H - [ info container | padding | line chart container ]
container.addArrangedSubview(infoContainer)
// info container: V - [ primary | secondary ]
infoContainer.addArrangedSubview(primaryLabel)
infoContainer.addArrangedSubview(secondaryLabel)
// padding
let padding = UIView()
container.addArrangedSubview(padding)
// line chart
container.addArrangedSubview(lineChartContainer)
let lineChartViewTopPadding = UIView()
let lineChartViewBottomPadding = UIView()
lineChartViewTopPadding.translatesAutoresizingMaskIntoConstraints = false
lineChartViewBottomPadding.translatesAutoresizingMaskIntoConstraints = false
lineChartView.translatesAutoresizingMaskIntoConstraints = false
lineChartContainer.addArrangedSubview(lineChartViewTopPadding)
lineChartContainer.addArrangedSubview(lineChartView)
lineChartContainer.addArrangedSubview(lineChartViewBottomPadding)
NSLayoutConstraint.activate([
lineChartView.widthAnchor.constraint(equalToConstant: 50),
lineChartView.heightAnchor.constraint(equalToConstant: 26),
lineChartViewTopPadding.heightAnchor.constraint(equalTo: lineChartViewBottomPadding.heightAnchor),
])
}
}

View File

@ -7,12 +7,11 @@
import UIKit
import Accelerate
import simd
import MastodonAsset
final class LineChartView: UIView {
public final class LineChartView: UIView {
var data: [CGFloat] = [] {
public var data: [CGFloat] = [] {
didSet {
setNeedsLayout()
}
@ -20,14 +19,13 @@ final class LineChartView: UIView {
let lineShapeLayer = CAShapeLayer()
let gradientLayer = CAGradientLayer()
// let dotShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
public override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
public required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
@ -38,10 +36,8 @@ extension LineChartView {
private func _init() {
lineShapeLayer.frame = bounds
gradientLayer.frame = bounds
// dotShapeLayer.frame = bounds
layer.addSublayer(lineShapeLayer)
layer.addSublayer(gradientLayer)
// layer.addSublayer(dotShapeLayer)
gradientLayer.colors = [
Asset.Colors.brandBlue.color.withAlphaComponent(0.5).cgColor, // set the same alpha to fill
@ -51,16 +47,14 @@ extension LineChartView {
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
}
override func layoutSubviews() {
public override func layoutSubviews() {
super.layoutSubviews()
lineShapeLayer.frame = bounds
gradientLayer.frame = bounds
// dotShapeLayer.frame = bounds
guard data.count > 1 else {
lineShapeLayer.path = nil
// dotShapeLayer.path = nil
gradientLayer.isHidden = true
return
}
@ -113,9 +107,5 @@ extension LineChartView {
maskLayer.strokeColor = UIColor.clear.cgColor
maskLayer.lineWidth = 0.0
gradientLayer.mask = maskLayer
// dotShapeLayer.lineWidth = 3
// dotShapeLayer.fillColor = Asset.Colors.brandBlue.color.cgColor
// dotShapeLayer.path = dotPath.cgPath
}
}

View File

@ -0,0 +1,56 @@
//
// NewsTableViewCell.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
public final class NewsTableViewCell: UITableViewCell {
public let newsView = NewsView()
let separatorLine = UIView.separatorLine
public override func prepareForReuse() {
super.prepareForReuse()
newsView.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 NewsTableViewCell {
private func _init() {
newsView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(newsView)
NSLayoutConstraint.activate([
newsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
newsView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
newsView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: newsView.bottomAnchor, constant: 16),
])
separatorLine.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(separatorLine)
NSLayoutConstraint.activate([
separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
])
}
}

View File

@ -0,0 +1,86 @@
//
// TrendTableViewCell.swift
//
//
// Created by MainasuK on 2022-4-13.
//
import UIKit
public final class TrendTableViewCell: UITableViewCell {
public let trendView = TrendView()
let separatorLine = UIView.separatorLine
public override func prepareForReuse() {
super.prepareForReuse()
configureSeparator(style: .inset)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension TrendTableViewCell {
private func _init() {
trendView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(trendView)
NSLayoutConstraint.activate([
trendView.topAnchor.constraint(equalTo: contentView.topAnchor),
trendView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
trendView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
trendView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
configureSeparator(style: .inset)
accessibilityElements = [trendView]
}
}
extension TrendTableViewCell {
public enum SeparatorStyle {
case edge
case inset
}
public func configureSeparator(style: SeparatorStyle) {
separatorLine.removeFromSuperview()
separatorLine.removeConstraints(separatorLine.constraints)
switch style {
case .edge:
separatorLine.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(separatorLine)
NSLayoutConstraint.activate([
separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
])
case .inset:
separatorLine.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(separatorLine)
NSLayoutConstraint.activate([
separatorLine.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
])
}
}
}

View File

@ -12,6 +12,7 @@ import MastodonUI
import SwiftUI
import MastodonAsset
import MastodonLocalization
import MastodonUI
class ShareViewController: UIViewController {

View File

@ -16,6 +16,7 @@ import SwiftUI
import UniformTypeIdentifiers
import MastodonAsset
import MastodonLocalization
import MastodonUI
final class ShareViewModel {

View File

@ -12,6 +12,7 @@ import MastodonSDK
import MastodonUI
import MastodonAsset
import MastodonLocalization
import MastodonUI
protocol ComposeToolbarViewDelegate: AnyObject {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton)