diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e596b10..6b923056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Blocking users via a post +- Reporting posts +- Blocking users via a comment +- Reporting comments +- Blocking communities +- Warning to notify users about errors performing actions outside of home instance + +### Misc + +- Removed `Shiny` package due to lag +- Removed `Local Console` package +- Cleaned up API fetchers code + ## [2023.12.1] ### Added diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 87e3991c..b13efa56 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -8,8 +8,6 @@ [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults -[LocalConsole](https://github.com/duraidabdul/LocalConsole) - In-app console and debug tools - [MarkdownUI](https://github.com/gonzalezreal/swift-markdown-ui) - Display and customize Markdown text in SwiftUI [Nuke](https://github.com/kean/Nuke) - Image loading system @@ -20,6 +18,4 @@ [SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols) - Access Apple's SF Symbols using static typing -[Shiny](https://github.com/maustinstar/shiny) - Simulate lighting and motion effects on color - [WhatsNewKit](https://github.com/SvenTiigi/WhatsNewKit) - Showcase your new app features \ No newline at end of file diff --git a/Lunar.xcodeproj/project.pbxproj b/Lunar.xcodeproj/project.pbxproj index 2d0d6e72..821b9fb1 100644 --- a/Lunar.xcodeproj/project.pbxproj +++ b/Lunar.xcodeproj/project.pbxproj @@ -48,7 +48,6 @@ 3C1A3F5F2A73D64C00898FC6 /* LoginHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3F5E2A73D64C00898FC6 /* LoginHelper.swift */; }; 3C1AF0AA2AB4BDA000763602 /* NestedCommentsLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1AF0A92AB4BDA000763602 /* NestedCommentsLogic.swift */; }; 3C20B37C2AEAF645003F578F /* DebugTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C20B37B2AEAF645003F578F /* DebugTextView.swift */; }; - 3C20B37F2AEB27F9003F578F /* LocalConsole in Frameworks */ = {isa = PBXBuildFile; productRef = 3C20B37E2AEB27F9003F578F /* LocalConsole */; }; 3C23F03E2AAA66E000AACA46 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 3C23F03D2AAA66E000AACA46 /* Pulse */; }; 3C23F0402AAA66E000AACA46 /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3C23F03F2AAA66E000AACA46 /* PulseUI */; }; 3C29F5452A97D49C001563E7 /* LunarUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C29F5442A97D49C001563E7 /* LunarUITests.swift */; }; @@ -78,6 +77,13 @@ 3C2EA6CE2B002DCA007AC12D /* ImageDownloadRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2EA6CC2B002DCA007AC12D /* ImageDownloadRetriever.swift */; }; 3C2EA6CF2B003B0F007AC12D /* URLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEF4CD42A69B798002B83C0 /* URLParser.swift */; }; 3C2EA6D02B003B45007AC12D /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C87092B2A745CE2004999CE /* ArrayExtension.swift */; }; + 3C3E75F42B210AF900F25F7D /* PulseWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75F32B210AF900F25F7D /* PulseWriter.swift */; }; + 3C3E75F62B21102200F25F7D /* RealmWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75F52B21102200F25F7D /* RealmWriter.swift */; }; + 3C3E75F82B21158100F25F7D /* GenerateHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75F72B21158100F25F7D /* GenerateHeaders.swift */; }; + 3C3E75FA2B21239400F25F7D /* BlockSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75F92B21239400F25F7D /* BlockSender.swift */; }; + 3C3E75FC2B212F2600F25F7D /* BlockResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75FB2B212F2600F25F7D /* BlockResponseModel.swift */; }; + 3C3E75FE2B22713400F25F7D /* ReportSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75FD2B22713400F25F7D /* ReportSender.swift */; }; + 3C3E76002B22726900F25F7D /* ReportPostResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E75FF2B22726900F25F7D /* ReportPostResponseModel.swift */; }; 3C41EEC82AFECEF200F47931 /* CheckBiometricType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C41EEC72AFECEF200F47931 /* CheckBiometricType.swift */; }; 3C42AD1A2A8038300056AFBC /* KbinCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C42AD192A8038300056AFBC /* KbinCommentsView.swift */; }; 3C42AD1C2A803AE50056AFBC /* KbinCommentsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C42AD1B2A803AE50056AFBC /* KbinCommentsFetcher.swift */; }; @@ -90,7 +96,6 @@ 3C4DB8C22AB70856000137CF /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 3C4DB8C12AB70856000137CF /* NukeExtensions */; }; 3C4DB8C62AB72145000137CF /* SettingsAppResetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4DB8C52AB72145000137CF /* SettingsAppResetView.swift */; }; 3C4DB8C82AB725FB000137CF /* DataCacheHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4DB8C72AB725FB000137CF /* DataCacheHolder.swift */; }; - 3C4E4CF92B1E68D50001D5FF /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 3C4E4CF82B1E68D50001D5FF /* Moya */; }; 3C4EBFF92B00F735004B7665 /* SupportedFamiliesExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4EBFF82B00F735004B7665 /* SupportedFamiliesExtension.swift */; }; 3C4EE4B92A8006D0009EE4A8 /* KbinPostRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4EE4B82A8006D0009EE4A8 /* KbinPostRowView.swift */; }; 3C4F0A002A54399F009DF8AB /* LunarAppEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4F09FF2A54399F009DF8AB /* LunarAppEntryView.swift */; }; @@ -144,7 +149,6 @@ 3C96EA552A7857D900753199 /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C96EA542A7857D900753199 /* AccountModel.swift */; }; 3C9E548A2A940F9B0093DDC7 /* UITextViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9E54892A940F9B0093DDC7 /* UITextViewExtension.swift */; }; 3C9E548C2A94A3DC0093DDC7 /* CommentSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9E548B2A94A3DC0093DDC7 /* CommentSender.swift */; }; - 3C9E54982A953FB60093DDC7 /* SettingsSplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9E54972A953FB60093DDC7 /* SettingsSplashScreenView.swift */; }; 3C9E549A2A954C060093DDC7 /* CommentResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9E54992A954C060093DDC7 /* CommentResponseModel.swift */; }; 3CA5C40A2A574FF500BF8F1B /* PostModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA5C4092A574FF500BF8F1B /* PostModel.swift */; }; 3CA5C4102A5753AC00BF8F1B /* LegacyPostRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA5C40F2A5753AC00BF8F1B /* LegacyPostRowView.swift */; }; @@ -201,7 +205,6 @@ 3CC178952A7DAAE300C208D0 /* SearchFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC178942A7DAAE300C208D0 /* SearchFetcher.swift */; }; 3CC3E4162ADBFCBA00F7F9A9 /* MyUserPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC3E4152ADBFCBA00F7F9A9 /* MyUserPostsView.swift */; }; 3CC3E4182ADBFFEF00F7F9A9 /* MyUserCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC3E4172ADBFFEF00F7F9A9 /* MyUserCommentsView.swift */; }; - 3CC3E41B2ADC054000F7F9A9 /* Shiny in Frameworks */ = {isa = PBXBuildFile; productRef = 3CC3E41A2ADC054000F7F9A9 /* Shiny */; }; 3CC3E41F2ADC36D900F7F9A9 /* DismissButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC3E41E2ADC36D900F7F9A9 /* DismissButtonView.swift */; }; 3CC3E4212ADC3FD600F7F9A9 /* CreatePostPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC3E4202ADC3FD600F7F9A9 /* CreatePostPopoverView.swift */; }; 3CC3E4232ADC4A2600F7F9A9 /* ImageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC3E4222ADC4A2600F7F9A9 /* ImageSender.swift */; }; @@ -220,7 +223,7 @@ 3CC3E4412AE047FE00F7F9A9 /* RealmPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC3E4402AE047FE00F7F9A9 /* RealmPost.swift */; }; 3CC4200A2AB37BCB00BE1612 /* WhatsNewKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3CC420092AB37BCB00BE1612 /* WhatsNewKit */; }; 3CC4200D2AB37DDA00BE1612 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 3CC4200C2AB37DDA00BE1612 /* BetterSafariView */; }; - 3CC734A42A6D58C6007523B5 /* URLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC734A32A6D58C6007523B5 /* URLBuilder.swift */; }; + 3CC734A42A6D58C6007523B5 /* EndpointBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC734A32A6D58C6007523B5 /* EndpointBuilder.swift */; }; 3CC734A62A6D591A007523B5 /* URLComponentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC734A52A6D591A007523B5 /* URLComponentsExtension.swift */; }; 3CC734AA2A6D697C007523B5 /* InsertSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC734A92A6D697C007523B5 /* InsertSorter.swift */; }; 3CCAA8BF2A65309E007CCEC2 /* NoInternetConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCAA8BE2A65309E007CCEC2 /* NoInternetConnectionView.swift */; }; @@ -265,7 +268,6 @@ 3CEF4CD02A69B738002B83C0 /* InPostMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEF4CCF2A69B738002B83C0 /* InPostMetadataView.swift */; }; 3CEF4CD32A69B770002B83C0 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEF4CD22A69B770002B83C0 /* StringExtension.swift */; }; 3CEF4CD52A69B798002B83C0 /* URLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEF4CD42A69B798002B83C0 /* URLParser.swift */; }; - 3CF3B8F12A8F797D001C08B5 /* SettingsLayoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF3B8F02A8F797D001C08B5 /* SettingsLayoutView.swift */; }; 3CF3B8F32A8FF7D4001C08B5 /* KbinSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF3B8F22A8FF7D4001C08B5 /* KbinSelectorView.swift */; }; 3CF3B8FD2A90188D001C08B5 /* CommentMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF3B8FC2A90188D001C08B5 /* CommentMetadataView.swift */; }; 3CF3F9C02A6EE0390051E725 /* SettingsClearCacheView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF3F9BF2A6EE0390051E725 /* SettingsClearCacheView.swift */; }; @@ -274,11 +276,6 @@ 3CF717FB2AF42ED2005030D8 /* RealmCommunity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF717FA2AF42ED2005030D8 /* RealmCommunity.swift */; }; 3CF717FD2AF437D4005030D8 /* LegacyCommunitiesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF717FC2AF437D3005030D8 /* LegacyCommunitiesFetcher.swift */; }; 3CF717FF2AF44704005030D8 /* LegacyCommunityRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF717FE2AF44704005030D8 /* LegacyCommunityRowView.swift */; }; - 3CF831BF2B1F9D960036BCE0 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF831BE2B1F9D960036BCE0 /* APIService.swift */; }; - 3CF831C52B1FA3E40036BCE0 /* ParameterStructures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF831C42B1FA3E40036BCE0 /* ParameterStructures.swift */; }; - 3CF831C72B1FAE390036BCE0 /* PostSenderM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF831C62B1FAE390036BCE0 /* PostSenderM.swift */; }; - 3CF831C92B1FAE5A0036BCE0 /* AppDelegateM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF831C82B1FAE5A0036BCE0 /* AppDelegateM.swift */; }; - 3CF831CB2B1FAE730036BCE0 /* CreatePostPopoverViewM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF831CA2B1FAE730036BCE0 /* CreatePostPopoverViewM.swift */; }; 3CFA769B2B127E76002B88B2 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA769A2B127E76002B88B2 /* MockData.swift */; }; 3CFB89E82AF19DC3001BB9EB /* CreateCommentPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB89E72AF19DC3001BB9EB /* CreateCommentPopover.swift */; }; 3CFE04BF2A7FEA71007BAD81 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 3CFE04BE2A7FEA71007BAD81 /* SwiftSoup */; }; @@ -369,6 +366,13 @@ 3C2EA6C22AFFFA42007AC12D /* WidgetLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetLink.swift; sourceTree = ""; }; 3C2EA6CA2B00274F007AC12D /* ImageDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloadManager.swift; sourceTree = ""; }; 3C2EA6CC2B002DCA007AC12D /* ImageDownloadRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloadRetriever.swift; sourceTree = ""; }; + 3C3E75F32B210AF900F25F7D /* PulseWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PulseWriter.swift; sourceTree = ""; }; + 3C3E75F52B21102200F25F7D /* RealmWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmWriter.swift; sourceTree = ""; }; + 3C3E75F72B21158100F25F7D /* GenerateHeaders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateHeaders.swift; sourceTree = ""; }; + 3C3E75F92B21239400F25F7D /* BlockSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockSender.swift; sourceTree = ""; }; + 3C3E75FB2B212F2600F25F7D /* BlockResponseModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockResponseModel.swift; sourceTree = ""; }; + 3C3E75FD2B22713400F25F7D /* ReportSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportSender.swift; sourceTree = ""; }; + 3C3E75FF2B22726900F25F7D /* ReportPostResponseModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportPostResponseModel.swift; sourceTree = ""; }; 3C41EEC72AFECEF200F47931 /* CheckBiometricType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBiometricType.swift; sourceTree = ""; }; 3C42AD192A8038300056AFBC /* KbinCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KbinCommentsView.swift; sourceTree = ""; }; 3C42AD1B2A803AE50056AFBC /* KbinCommentsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KbinCommentsFetcher.swift; sourceTree = ""; }; @@ -437,7 +441,6 @@ 3C96EA542A7857D900753199 /* AccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountModel.swift; sourceTree = ""; }; 3C9E54892A940F9B0093DDC7 /* UITextViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewExtension.swift; sourceTree = ""; }; 3C9E548B2A94A3DC0093DDC7 /* CommentSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentSender.swift; sourceTree = ""; }; - 3C9E54972A953FB60093DDC7 /* SettingsSplashScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSplashScreenView.swift; sourceTree = ""; }; 3C9E54992A954C060093DDC7 /* CommentResponseModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentResponseModel.swift; sourceTree = ""; }; 3CA5C4092A574FF500BF8F1B /* PostModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostModel.swift; sourceTree = ""; }; 3CA5C40F2A5753AC00BF8F1B /* LegacyPostRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyPostRowView.swift; sourceTree = ""; }; @@ -510,7 +513,7 @@ 3CC3E43A2ADDE56300F7F9A9 /* RegexPatterns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegexPatterns.swift; sourceTree = ""; }; 3CC3E43C2ADDF69200F7F9A9 /* URLInputViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLInputViews.swift; sourceTree = ""; }; 3CC3E4402AE047FE00F7F9A9 /* RealmPost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmPost.swift; sourceTree = ""; }; - 3CC734A32A6D58C6007523B5 /* URLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBuilder.swift; sourceTree = ""; }; + 3CC734A32A6D58C6007523B5 /* EndpointBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointBuilder.swift; sourceTree = ""; }; 3CC734A52A6D591A007523B5 /* URLComponentsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLComponentsExtension.swift; sourceTree = ""; }; 3CC734A92A6D697C007523B5 /* InsertSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSorter.swift; sourceTree = ""; }; 3CCAA8BE2A65309E007CCEC2 /* NoInternetConnectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoInternetConnectionView.swift; sourceTree = ""; }; @@ -548,7 +551,6 @@ 3CEF4CCF2A69B738002B83C0 /* InPostMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPostMetadataView.swift; sourceTree = ""; }; 3CEF4CD22A69B770002B83C0 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 3CEF4CD42A69B798002B83C0 /* URLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLParser.swift; sourceTree = ""; }; - 3CF3B8F02A8F797D001C08B5 /* SettingsLayoutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLayoutView.swift; sourceTree = ""; }; 3CF3B8F22A8FF7D4001C08B5 /* KbinSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KbinSelectorView.swift; sourceTree = ""; }; 3CF3B8FC2A90188D001C08B5 /* CommentMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentMetadataView.swift; sourceTree = ""; }; 3CF3F9BF2A6EE0390051E725 /* SettingsClearCacheView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsClearCacheView.swift; sourceTree = ""; }; @@ -557,11 +559,6 @@ 3CF717FA2AF42ED2005030D8 /* RealmCommunity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmCommunity.swift; sourceTree = ""; }; 3CF717FC2AF437D3005030D8 /* LegacyCommunitiesFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCommunitiesFetcher.swift; sourceTree = ""; }; 3CF717FE2AF44704005030D8 /* LegacyCommunityRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCommunityRowView.swift; sourceTree = ""; }; - 3CF831BE2B1F9D960036BCE0 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; - 3CF831C42B1FA3E40036BCE0 /* ParameterStructures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParameterStructures.swift; sourceTree = ""; }; - 3CF831C62B1FAE390036BCE0 /* PostSenderM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostSenderM.swift; sourceTree = ""; }; - 3CF831C82B1FAE5A0036BCE0 /* AppDelegateM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegateM.swift; sourceTree = ""; }; - 3CF831CA2B1FAE730036BCE0 /* CreatePostPopoverViewM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePostPopoverViewM.swift; sourceTree = ""; }; 3CFA769A2B127E76002B88B2 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; 3CFB89E72AF19DC3001BB9EB /* CreateCommentPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateCommentPopover.swift; sourceTree = ""; }; 3CFE04C22A7FF0EB007BAD81 /* KbinThreadsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KbinThreadsModel.swift; sourceTree = ""; }; @@ -593,18 +590,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3C4E4CF92B1E68D50001D5FF /* Moya in Frameworks */, 3CEB2C132AB2F45F0013609E /* SFSafeSymbols in Frameworks */, 3C23F0402AAA66E000AACA46 /* PulseUI in Frameworks */, 3CC4200D2AB37DDA00BE1612 /* BetterSafariView in Frameworks */, 3CE69E962ABDCA8A00E2821E /* Realm in Frameworks */, - 3CC3E41B2ADC054000F7F9A9 /* Shiny in Frameworks */, 3CDA82C62B1F7E420078AB36 /* Collections in Frameworks */, 3CA95F082A7ACF3B00626353 /* Nuke in Frameworks */, 3C4B7DDB2A58AD23003C8192 /* Alamofire in Frameworks */, 3CE69E982ABDCA8A00E2821E /* RealmSwift in Frameworks */, 3CC4200A2AB37BCB00BE1612 /* WhatsNewKit in Frameworks */, - 3C20B37F2AEB27F9003F578F /* LocalConsole in Frameworks */, 3C23F03E2AAA66E000AACA46 /* Pulse in Frameworks */, 3CEBF46C2B09426E00469008 /* MarkdownUI in Frameworks */, 3CDA82C82B1F7E420078AB36 /* OrderedCollections in Frameworks */, @@ -671,16 +665,31 @@ path = Widgets; sourceTree = ""; }; - 3C4D42A62A8D0FB3003B6F1D /* DataSenders */ = { + 3C3E75F12B20FF1300F25F7D /* Fetchers */ = { isa = PBXGroup; children = ( + 3CECCE222AAE1B1B0017A605 /* JWT.swift */, + 3C3E75F32B210AF900F25F7D /* PulseWriter.swift */, + 3C3E75F72B21158100F25F7D /* GenerateHeaders.swift */, + 3C3E75F52B21102200F25F7D /* RealmWriter.swift */, + 3CC734A32A6D58C6007523B5 /* EndpointBuilder.swift */, 3C4D42A72A8D0FD1003B6F1D /* VoteSender.swift */, 3CC3E4222ADC4A2600F7F9A9 /* ImageSender.swift */, 3C6BBF712A910F6100312E2B /* SubscriptionActionSender.swift */, + 3C3E75F92B21239400F25F7D /* BlockSender.swift */, + 3C3E75FD2B22713400F25F7D /* ReportSender.swift */, 3C9E548B2A94A3DC0093DDC7 /* CommentSender.swift */, 3CC3E42C2ADD6EBA00F7F9A9 /* PostSender.swift */, + 3CA8A0C22AE570E200EEABD3 /* PostsFetcher.swift */, + 3CC09CDA2B0E92840036422F /* PrivateMessageFetcher.swift */, + 3CB046F52ADB28850011BFEC /* PersonFetcher.swift */, + 3C65885C2A6189AF00A46DA3 /* CommentsFetcher.swift */, + 3CB408B32A5C558B00B7C983 /* CommunitiesFetcher.swift */, + 3C5ECB1E2A75ABFE002BA561 /* SiteInfoFetcher.swift */, + 3CC3E4342ADDD74D00F7F9A9 /* PostSiteMetadataFetcher.swift */, + 3CC178942A7DAAE300C208D0 /* SearchFetcher.swift */, ); - path = DataSenders; + path = Fetchers; sourceTree = ""; }; 3C4F09F32A54399F009DF8AB = { @@ -714,7 +723,6 @@ 3C62C23F2A72AABD003BAC5B /* Info.plist */, 3C62C23E2A72AA4D003BAC5B /* Lunar.entitlements */, 3C808E222A9A8C95008FA62E /* LunarDebug.entitlements */, - 3CF831C32B1FA3D10036BCE0 /* APIService */, 3CFA769A2B127E76002B88B2 /* MockData.swift */, 3C2EA6C22AFFFA42007AC12D /* WidgetLink.swift */, 3C4F09FF2A54399F009DF8AB /* LunarAppEntryView.swift */, @@ -728,9 +736,8 @@ 3CC09CE62B0E9C480036422F /* Inbox Tab */, 3CECCE212AAE14140017A605 /* Account Tab */, 3CCAE1842A6ADDE20040EE12 /* Comment View */, + 3C3E75F12B20FF1300F25F7D /* Fetchers */, 3CE237332AA4D157000F1D20 /* Common Views */, - 3C4D42A62A8D0FB3003B6F1D /* DataSenders */, - 3CA5C4062A5743EB00BF8F1B /* DataLoaders */, 3CA5C4082A57443200BF8F1B /* DataModels */, 3C96EA582A79147C00753199 /* Debugging Views */, 3CC3E4102ADBE39700F7F9A9 /* Defaults */, @@ -784,53 +791,6 @@ path = "Debugging Views"; sourceTree = ""; }; - 3C9E548F2A94C0870093DDC7 /* Account */ = { - isa = PBXGroup; - children = ( - 3C6D29682A797D6E007AFD99 /* AccountItemView.swift */, - 3C113C422A6EA92600FDE5CE /* InstanceSelectorView.swift */, - 3CAEDDE42A9E58DB001469AA /* ManageInstancesView.swift */, - 3CF3B8F22A8FF7D4001C08B5 /* KbinSelectorView.swift */, - ); - name = Account; - sourceTree = ""; - }; - 3C9E54952A94C3170093DDC7 /* Developer Options */ = { - isa = PBXGroup; - children = ( - 3C2C45492AA648A000727DD5 /* UserDefaultsExplorerView.swift */, - 3CECCE1B2AADBFFA0017A605 /* ColorTesterView.swift */, - 3CA8A0C82AE5DBA900EEABD3 /* RealmBrowser.swift */, - ); - name = "Developer Options"; - sourceTree = ""; - }; - 3C9E54962A94C34D0093DDC7 /* Appearance */ = { - isa = PBXGroup; - children = ( - 3C87A3692A9F9C4E0029C18F /* ForceAppearance.swift */, - 3C9E54972A953FB60093DDC7 /* SettingsSplashScreenView.swift */, - 3C2B14742AABC7CB0028E05A /* SettingsQuicklinksView.swift */, - 3CECCE1D2AADDBBA0017A605 /* CircleFillIcons.swift */, - ); - name = Appearance; - sourceTree = ""; - }; - 3CA5C4062A5743EB00BF8F1B /* DataLoaders */ = { - isa = PBXGroup; - children = ( - 3CA8A0C22AE570E200EEABD3 /* PostsFetcher.swift */, - 3CC09CDA2B0E92840036422F /* PrivateMessageFetcher.swift */, - 3CB046F52ADB28850011BFEC /* PersonFetcher.swift */, - 3C65885C2A6189AF00A46DA3 /* CommentsFetcher.swift */, - 3CB408B32A5C558B00B7C983 /* CommunitiesFetcher.swift */, - 3C5ECB1E2A75ABFE002BA561 /* SiteInfoFetcher.swift */, - 3CC3E4342ADDD74D00F7F9A9 /* PostSiteMetadataFetcher.swift */, - 3CC178942A7DAAE300C208D0 /* SearchFetcher.swift */, - ); - path = DataLoaders; - sourceTree = ""; - }; 3CA5C4082A57443200BF8F1B /* DataModels */ = { isa = PBXGroup; children = ( @@ -902,6 +862,8 @@ 3CC3E4302ADDCE1200F7F9A9 /* CreatePostResponseModel.swift */, 3CC3E4242ADC556300F7F9A9 /* ImageUploadResponseModel.swift */, 3C6BBF732A91108A00312E2B /* SubscribeResponseModel.swift */, + 3C3E75FB2B212F2600F25F7D /* BlockResponseModel.swift */, + 3C3E75FF2B22726900F25F7D /* ReportPostResponseModel.swift */, 3C9E54992A954C060093DDC7 /* CommentResponseModel.swift */, 3C1A3F5C2A73BC9F00898FC6 /* CredentialsModel.swift */, 3CC3E4322ADDCF7E00F7F9A9 /* ErrorResponseModel.swift */, @@ -1003,7 +965,6 @@ children = ( 3CE35B132AE2AFD600263E13 /* MailtoUtil.swift */, 3CEF4CD42A69B798002B83C0 /* URLParser.swift */, - 3CC734A32A6D58C6007523B5 /* URLBuilder.swift */, 3CC734A92A6D697C007523B5 /* InsertSorter.swift */, 3C96EA412A77225E00753199 /* ValidationUtils.swift */, 3C1A3F5A2A73B2B800898FC6 /* KeychainInterface.swift */, @@ -1087,7 +1048,6 @@ 3C11B28A2B1CB44600244838 /* SettingsAppearanceView.swift */, 3C113C382A6E71E100FDE5CE /* SettingsAccountView.swift */, 3C87A36D2A9FBDE70029C18F /* SettingsAppIconPickerView.swift */, - 3CF3B8F02A8F797D001C08B5 /* SettingsLayoutView.swift */, 3C87A3672A9F999D0029C18F /* SettingsThemeView.swift */, 3C808E232A9B6DAD008FA62E /* SettingsDevOptionsView.swift */, 3CE35B152AE2D7CD00263E13 /* SettingsAdditionalView.swift */, @@ -1096,10 +1056,18 @@ 3C6ACC212AE5481F0073288E /* SettingsClearRealmView.swift */, 3C4DB8C52AB72145000137CF /* SettingsAppResetView.swift */, 3C2A80582AFD88C70077B7D7 /* HiddenPostsGuardView.swift */, - 3C9E548F2A94C0870093DDC7 /* Account */, - 3C9E54962A94C34D0093DDC7 /* Appearance */, - 3CE35B1D2AE3D7E100263E13 /* Additional Options */, - 3C9E54952A94C3170093DDC7 /* Developer Options */, + 3C6D29682A797D6E007AFD99 /* AccountItemView.swift */, + 3C113C422A6EA92600FDE5CE /* InstanceSelectorView.swift */, + 3CAEDDE42A9E58DB001469AA /* ManageInstancesView.swift */, + 3CF3B8F22A8FF7D4001C08B5 /* KbinSelectorView.swift */, + 3C87A3692A9F9C4E0029C18F /* ForceAppearance.swift */, + 3C2B14742AABC7CB0028E05A /* SettingsQuicklinksView.swift */, + 3CECCE1D2AADDBBA0017A605 /* CircleFillIcons.swift */, + 3CE35B1B2AE3D7D600263E13 /* AttributionsView.swift */, + 3CE35B1E2AE3DAF000263E13 /* AttributionInfo.swift */, + 3C2C45492AA648A000727DD5 /* UserDefaultsExplorerView.swift */, + 3CECCE1B2AADBFFA0017A605 /* ColorTesterView.swift */, + 3CA8A0C82AE5DBA900EEABD3 /* RealmBrowser.swift */, ); path = "Settings Views"; sourceTree = ""; @@ -1113,7 +1081,6 @@ 3CB8D2652A55EFF900EE615A /* PlaceholderView.swift */, 3C130F762A8C0CB90060B6E4 /* ReactionButton.swift */, 3CE35B172AE2FF1B00263E13 /* PostButtonItem.swift */, - 3CECCE222AAE1B1B0017A605 /* JWT.swift */, 3CCAA8BE2A65309E007CCEC2 /* NoInternetConnectionView.swift */, 3CE237342AA4D171000F1D20 /* LargeNavButton.swift */, 3CE237362AA4D188000F1D20 /* SmallNavButton.swift */, @@ -1123,15 +1090,6 @@ path = "Common Views"; sourceTree = ""; }; - 3CE35B1D2AE3D7E100263E13 /* Additional Options */ = { - isa = PBXGroup; - children = ( - 3CE35B1B2AE3D7D600263E13 /* AttributionsView.swift */, - 3CE35B1E2AE3DAF000263E13 /* AttributionInfo.swift */, - ); - name = "Additional Options"; - sourceTree = ""; - }; 3CECCE212AAE14140017A605 /* Account Tab */ = { isa = PBXGroup; children = ( @@ -1143,18 +1101,6 @@ path = "Account Tab"; sourceTree = ""; }; - 3CF831C32B1FA3D10036BCE0 /* APIService */ = { - isa = PBXGroup; - children = ( - 3CF831CA2B1FAE730036BCE0 /* CreatePostPopoverViewM.swift */, - 3CF831C82B1FAE5A0036BCE0 /* AppDelegateM.swift */, - 3CF831C62B1FAE390036BCE0 /* PostSenderM.swift */, - 3CF831BE2B1F9D960036BCE0 /* APIService.swift */, - 3CF831C42B1FA3E40036BCE0 /* ParameterStructures.swift */, - ); - path = APIService; - sourceTree = ""; - }; 3CFE04C42A7FF1E7007BAD81 /* Kbin */ = { isa = PBXGroup; children = ( @@ -1243,10 +1189,7 @@ 3C4DB8C12AB70856000137CF /* NukeExtensions */, 3CE69E952ABDCA8A00E2821E /* Realm */, 3CE69E972ABDCA8A00E2821E /* RealmSwift */, - 3CC3E41A2ADC054000F7F9A9 /* Shiny */, - 3C20B37E2AEB27F9003F578F /* LocalConsole */, 3CEBF46B2B09426E00469008 /* MarkdownUI */, - 3C4E4CF82B1E68D50001D5FF /* Moya */, 3CDA82C52B1F7E420078AB36 /* Collections */, 3CDA82C72B1F7E420078AB36 /* OrderedCollections */, ); @@ -1295,20 +1238,17 @@ ); mainGroup = 3C4F09F32A54399F009DF8AB; packageReferences = ( - 3C4B7DD92A58AD23003C8192 /* XCRemoteSwiftPackageReference "Alamofire.git" */, - 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke.git" */, + 3C4B7DD92A58AD23003C8192 /* XCRemoteSwiftPackageReference "Alamofire" */, + 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke" */, 3CFE04BD2A7FEA71007BAD81 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 3C23F03C2AAA66E000AACA46 /* XCRemoteSwiftPackageReference "Pulse" */, 3CEB2C082AB2F3A50013609E /* XCRemoteSwiftPackageReference "Defaults" */, - 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols.git" */, + 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, 3CC420082AB37BCB00BE1612 /* XCRemoteSwiftPackageReference "WhatsNewKit" */, 3CC4200B2AB37DDA00BE1612 /* XCRemoteSwiftPackageReference "BetterSafariView" */, - 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift.git" */, - 3CC3E4192ADC054000F7F9A9 /* XCRemoteSwiftPackageReference "shiny" */, - 3C20B37D2AEB27F8003F578F /* XCRemoteSwiftPackageReference "LocalConsole" */, + 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift" */, 3CEBF46A2B09426E00469008 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, - 3C4E4CF72B1E68D50001D5FF /* XCRemoteSwiftPackageReference "Moya.git" */, - 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections.git" */, + 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections" */, ); productRefGroup = 3C4F09FD2A54399F009DF8AB /* Products */; projectDirPath = ""; @@ -1428,6 +1368,7 @@ 3CB9C7312AA244CC00FB5A10 /* Post.swift in Sources */, 3CEF4CD02A69B738002B83C0 /* InPostMetadataView.swift in Sources */, 3CB9C7442AA24B4100FB5A10 /* MyUser.swift in Sources */, + 3C3E75F62B21102200F25F7D /* RealmWriter.swift in Sources */, 3C2B14752AABC7CB0028E05A /* SettingsQuicklinksView.swift in Sources */, 3C5ECB2A2A7676AD002BA561 /* UsernameFieldView.swift in Sources */, 3CA95F052A7ACBD500626353 /* PhotoDetailView.swift in Sources */, @@ -1460,15 +1401,17 @@ 3CECCE252AAE68E80017A605 /* LocalUserProperties.swift in Sources */, 3C87A36A2A9F9C4E0029C18F /* ForceAppearance.swift in Sources */, 3C5ECB2C2A7676B5002BA561 /* PasswordFieldView.swift in Sources */, + 3C3E75FA2B21239400F25F7D /* BlockSender.swift in Sources */, 3CB046F82ADB2A220011BFEC /* PersonModel.swift in Sources */, 3CA5C40A2A574FF500BF8F1B /* PostModel.swift in Sources */, 3CB9C72B2AA2423400FB5A10 /* CommentObject.swift in Sources */, + 3C3E75F42B210AF900F25F7D /* PulseWriter.swift in Sources */, 3C5D66BA2B0A8CB200DBBE87 /* NonRealmRecursiveComment.swift in Sources */, 3CC3E4162ADBFCBA00F7F9A9 /* MyUserPostsView.swift in Sources */, 3C87092C2A745CE2004999CE /* ArrayExtension.swift in Sources */, 3C2EA69E2AFFDE87007AC12D /* AccountWidget.swift in Sources */, 3C4D42AA2A8D12B8003B6F1D /* VoteResponseModel.swift in Sources */, - 3CC734A42A6D58C6007523B5 /* URLBuilder.swift in Sources */, + 3CC734A42A6D58C6007523B5 /* EndpointBuilder.swift in Sources */, 3CDAFF612B12547300C83BE3 /* OfflineDownloaderView.swift in Sources */, 3CC3E42F2ADDC59400F7F9A9 /* ImageUploaderView.swift in Sources */, 3C54C09D2ABA1ECE00F1BADE /* Defaults.swift in Sources */, @@ -1491,6 +1434,7 @@ 3C9E549A2A954C060093DDC7 /* CommentResponseModel.swift in Sources */, 3C2EA6CB2B00274F007AC12D /* ImageDownloadManager.swift in Sources */, 3CC178952A7DAAE300C208D0 /* SearchFetcher.swift in Sources */, + 3C3E75FE2B22713400F25F7D /* ReportSender.swift in Sources */, 3CF3F9C02A6EE0390051E725 /* SettingsClearCacheView.swift in Sources */, 3CFE04C72A7FF270007BAD81 /* KbinThreadsFetcher.swift in Sources */, 3C96EA502A7843EC00753199 /* AddNewUserButtonView.swift in Sources */, @@ -1500,7 +1444,6 @@ 3CEF4CC22A69B38B002B83C0 /* SubscribedCommunitiesView.swift in Sources */, 3CB046F22ADAF3C00011BFEC /* CommentsViewWorkaroundWarning.swift in Sources */, 3CA8A0C92AE5DBA900EEABD3 /* RealmBrowser.swift in Sources */, - 3CF831C52B1FA3E40036BCE0 /* ParameterStructures.swift in Sources */, 3CBF88122AEEC8C20058B14D /* LegacyInPostActionsView.swift in Sources */, 3C4DB8BE2AB701B2000137CF /* AppDelegate.swift in Sources */, 3CC3E4412AE047FE00F7F9A9 /* RealmPost.swift in Sources */, @@ -1528,19 +1471,19 @@ 3C6ACC202AE52FD20073288E /* RichLinks.swift in Sources */, 3C54C0A12ABB995E00F1BADE /* DefaultValues.swift in Sources */, 3C5ECB1F2A75ABFE002BA561 /* SiteInfoFetcher.swift in Sources */, - 3CF831C72B1FAE390036BCE0 /* PostSenderM.swift in Sources */, - 3CF831CB2B1FAE730036BCE0 /* CreatePostPopoverViewM.swift in Sources */, 3C9E548C2A94A3DC0093DDC7 /* CommentSender.swift in Sources */, 3CF43AA92AE1A42A006C8E84 /* PostsView.swift in Sources */, 3CC09CE12B0E95490036422F /* PrivateMessage.swift in Sources */, 3CE35B1F2AE3DAF000263E13 /* AttributionInfo.swift in Sources */, 3CE237352AA4D171000F1D20 /* LargeNavButton.swift in Sources */, 3CE35B142AE2AFD600263E13 /* MailtoUtil.swift in Sources */, + 3C3E76002B22726900F25F7D /* ReportPostResponseModel.swift in Sources */, 3C62C2552A72F4BC003BAC5B /* SplashScreenView.swift in Sources */, 3C6D29652A793878007AFD99 /* DictionaryExtension.swift in Sources */, 3CECCE1C2AADBFFA0017A605 /* ColorTesterView.swift in Sources */, 3CC3E4232ADC4A2600F7F9A9 /* ImageSender.swift in Sources */, 3C808E242A9B6DAD008FA62E /* SettingsDevOptionsView.swift in Sources */, + 3C3E75FC2B212F2600F25F7D /* BlockResponseModel.swift in Sources */, 3C42AD1A2A8038300056AFBC /* KbinCommentsView.swift in Sources */, 3C6299D12A751A5900BE2A9F /* TextExtension.swift in Sources */, 3CF3B8FD2A90188D001C08B5 /* CommentMetadataView.swift in Sources */, @@ -1571,13 +1514,12 @@ 3C5ECB242A766D64002BA561 /* DebugLoginPagePropertiesView.swift in Sources */, 3CC734A62A6D591A007523B5 /* URLComponentsExtension.swift in Sources */, 3C2A80592AFD88C70077B7D7 /* HiddenPostsGuardView.swift in Sources */, - 3C9E54982A953FB60093DDC7 /* SettingsSplashScreenView.swift in Sources */, 3CEF4CD52A69B798002B83C0 /* URLParser.swift in Sources */, - 3CF831C92B1FAE5A0036BCE0 /* AppDelegateM.swift in Sources */, 3CC3E4292ADC7B7A00F7F9A9 /* LocalImagePopoverView.swift in Sources */, 3CB9C7222AA23F7400FB5A10 /* CommunityObject.swift in Sources */, 3CA92E3B2A543BF000688DEB /* CommunityModel.swift in Sources */, 3CF3B8F32A8FF7D4001C08B5 /* KbinSelectorView.swift in Sources */, + 3C3E75F82B21158100F25F7D /* GenerateHeaders.swift in Sources */, 3CC3E41F2ADC36D900F7F9A9 /* DismissButtonView.swift in Sources */, 3C20B37C2AEAF645003F578F /* DebugTextView.swift in Sources */, 3CFA769B2B127E76002B88B2 /* MockData.swift in Sources */, @@ -1613,7 +1555,6 @@ 3CBCCDF62A589F2C00307AC1 /* CommentsView.swift in Sources */, 3C65885F2A621C6100A46DA3 /* ExploreCommunitiesView.swift in Sources */, 3CC09CDD2B0E92D90036422F /* PrivateMessageModel.swift in Sources */, - 3CF831BF2B1F9D960036BCE0 /* APIService.swift in Sources */, 3CC3E4352ADDD74D00F7F9A9 /* PostSiteMetadataFetcher.swift in Sources */, 3C6ACC1E2AE426D40073288E /* AboutLemmyInfo.swift in Sources */, 3C64E7C92AF700E9003BB42E /* WhatsNew_0_0_0.swift in Sources */, @@ -1625,7 +1566,6 @@ 3C1A3F592A73B17E00898FC6 /* LoginView.swift in Sources */, 3C1A3F552A731C3F00898FC6 /* ImageExtension.swift in Sources */, 3C41EEC82AFECEF200F47931 /* CheckBiometricType.swift in Sources */, - 3CF3B8F12A8F797D001C08B5 /* SettingsLayoutView.swift in Sources */, 3C2EA6C32AFFFA42007AC12D /* WidgetLink.swift in Sources */, 3C65885D2A6189AF00A46DA3 /* CommentsFetcher.swift in Sources */, 3CECCE192AACFA4B0017A605 /* MyUserView.swift in Sources */, @@ -2134,14 +2074,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 3C20B37D2AEB27F8003F578F /* XCRemoteSwiftPackageReference "LocalConsole" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/duraidabdul/LocalConsole/"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.12.1; - }; - }; 3C23F03C2AAA66E000AACA46 /* XCRemoteSwiftPackageReference "Pulse" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/Pulse"; @@ -2150,7 +2082,7 @@ minimumVersion = 4.0.3; }; }; - 3C4B7DD92A58AD23003C8192 /* XCRemoteSwiftPackageReference "Alamofire.git" */ = { + 3C4B7DD92A58AD23003C8192 /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; requirement = { @@ -2158,15 +2090,7 @@ minimumVersion = 5.7.1; }; }; - 3C4E4CF72B1E68D50001D5FF /* XCRemoteSwiftPackageReference "Moya.git" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Moya/Moya.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 15.0.3; - }; - }; - 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke.git" */ = { + 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/Nuke.git"; requirement = { @@ -2174,14 +2098,6 @@ minimumVersion = 12.1.5; }; }; - 3CC3E4192ADC054000F7F9A9 /* XCRemoteSwiftPackageReference "shiny" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/maustinstar/shiny"; - requirement = { - branch = master; - kind = branch; - }; - }; 3CC420082AB37BCB00BE1612 /* XCRemoteSwiftPackageReference "WhatsNewKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SvenTiigi/WhatsNewKit"; @@ -2198,7 +2114,7 @@ kind = branch; }; }; - 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections.git" */ = { + 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-collections.git"; requirement = { @@ -2206,7 +2122,7 @@ minimumVersion = 1.0.5; }; }; - 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift.git" */ = { + 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/realm-swift.git"; requirement = { @@ -2222,7 +2138,7 @@ kind = branch; }; }; - 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols.git" */ = { + 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols.git"; requirement = { @@ -2249,11 +2165,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 3C20B37E2AEB27F9003F578F /* LocalConsole */ = { - isa = XCSwiftPackageProductDependency; - package = 3C20B37D2AEB27F8003F578F /* XCRemoteSwiftPackageReference "LocalConsole" */; - productName = LocalConsole; - }; 3C23F03D2AAA66E000AACA46 /* Pulse */ = { isa = XCSwiftPackageProductDependency; package = 3C23F03C2AAA66E000AACA46 /* XCRemoteSwiftPackageReference "Pulse" */; @@ -2274,38 +2185,28 @@ }; 3C2EA6C82B002322007AC12D /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; - package = 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols.git" */; + package = 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; 3C4B7DDA2A58AD23003C8192 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; - package = 3C4B7DD92A58AD23003C8192 /* XCRemoteSwiftPackageReference "Alamofire.git" */; + package = 3C4B7DD92A58AD23003C8192 /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; 3C4DB8C12AB70856000137CF /* NukeExtensions */ = { isa = XCSwiftPackageProductDependency; productName = NukeExtensions; }; - 3C4E4CF82B1E68D50001D5FF /* Moya */ = { - isa = XCSwiftPackageProductDependency; - package = 3C4E4CF72B1E68D50001D5FF /* XCRemoteSwiftPackageReference "Moya.git" */; - productName = Moya; - }; 3CA95F072A7ACF3B00626353 /* Nuke */ = { isa = XCSwiftPackageProductDependency; - package = 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke.git" */; + package = 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke" */; productName = Nuke; }; 3CA95F092A7ACF3B00626353 /* NukeUI */ = { isa = XCSwiftPackageProductDependency; - package = 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke.git" */; + package = 3CA95F062A7ACF3B00626353 /* XCRemoteSwiftPackageReference "Nuke" */; productName = NukeUI; }; - 3CC3E41A2ADC054000F7F9A9 /* Shiny */ = { - isa = XCSwiftPackageProductDependency; - package = 3CC3E4192ADC054000F7F9A9 /* XCRemoteSwiftPackageReference "shiny" */; - productName = Shiny; - }; 3CC420092AB37BCB00BE1612 /* WhatsNewKit */ = { isa = XCSwiftPackageProductDependency; package = 3CC420082AB37BCB00BE1612 /* XCRemoteSwiftPackageReference "WhatsNewKit" */; @@ -2318,22 +2219,22 @@ }; 3CDA82C52B1F7E420078AB36 /* Collections */ = { isa = XCSwiftPackageProductDependency; - package = 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections.git" */; + package = 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = Collections; }; 3CDA82C72B1F7E420078AB36 /* OrderedCollections */ = { isa = XCSwiftPackageProductDependency; - package = 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections.git" */; + package = 3CDA82C42B1F7E420078AB36 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; 3CE69E952ABDCA8A00E2821E /* Realm */ = { isa = XCSwiftPackageProductDependency; - package = 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift.git" */; + package = 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift" */; productName = Realm; }; 3CE69E972ABDCA8A00E2821E /* RealmSwift */ = { isa = XCSwiftPackageProductDependency; - package = 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift.git" */; + package = 3CE69E942ABDCA8A00E2821E /* XCRemoteSwiftPackageReference "realm-swift" */; productName = RealmSwift; }; 3CEB2C092AB2F3A50013609E /* Defaults */ = { @@ -2343,7 +2244,7 @@ }; 3CEB2C122AB2F45F0013609E /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; - package = 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols.git" */; + package = 3CEB2C112AB2F45F0013609E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; 3CEBF46B2B09426E00469008 /* MarkdownUI */ = { diff --git a/Lunar/APIService/APIService.swift b/Lunar/APIService/APIService.swift deleted file mode 100644 index fe94b58d..00000000 --- a/Lunar/APIService/APIService.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// APIService.swift -// Lunar -// -// Created by Mani on 05/12/2023. -// - -import Foundation -import Moya - -let lemmyProvider = MoyaProvider() - -enum LemmyAPI { - case listPosts(parameters: ListPostsParameters) - case createPost(parameters: CreatePostParameters) -} - -extension LemmyAPI: TargetType { - var baseURL: URL { - return URL(string: "https://lemmy.world/api/v3")! - } - - var path: String { - switch self { - case .listPosts: return "/post/list" - case .createPost: return "/post" - } - } - - var method: Moya.Method { - switch self { - case .listPosts: .get - case .createPost: .post - } - } - -// var sampleData: Data { -// return Data() // You can provide sample data for testing -// } - - var task: Task { - switch self { - case .listPosts(let parameters): - var params: [String: Any] = [:] - if let type = parameters.type { params["type_"] = type } - if let sort = parameters.sort { params["sort"] = sort } - if let page = parameters.page { params["page"] = page } - if let limit = parameters.limit { params["limit"] = limit } - if let communityId = parameters.communityId { params["community_id"] = communityId } - if let communityName = parameters.communityName { params["community_name"] = communityName } - if let savedOnly = parameters.savedOnly { params["saved_only"] = savedOnly } - if let likedOnly = parameters.likedOnly { params["liked_only"] = likedOnly } - if let dislikedOnly = parameters.dislikedOnly { params["disliked_only"] = dislikedOnly } - if let pageCursor = parameters.pageCursor { params["page_cursor"] = pageCursor } - return .requestParameters(parameters: params, encoding: URLEncoding.default) - - case .createPost(parameters: let parameters): - var params: [String: Any] = [ - "name": parameters.name, - "community_id": parameters.communityId, - "language_id": parameters.languageId, - "auth": parameters.auth - ] - if let url = parameters.url { params["url"] = url } - if let body = parameters.body { params["body"] = body } - if let nsfw = parameters.nsfw { params["nsfw"] = nsfw } - return .requestParameters(parameters: params, encoding: JSONEncoding.default) - } - } - - var headers: [String: String]? { - switch self { - case .createPost(let parameters): - return ["Authorization": "Bearer \(parameters.auth)"] - default: - return ["Content-Type": "application/json"] - } - } -} diff --git a/Lunar/APIService/AppDelegateM.swift b/Lunar/APIService/AppDelegateM.swift deleted file mode 100644 index 9aa84821..00000000 --- a/Lunar/APIService/AppDelegateM.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// AppDelegate.swift -// Lunar -// -// Created by Mani on 17/09/2023. -// - -import Defaults -import Foundation -import LocalConsole -import Nuke -import RealmSwift -import SwiftUI - -class AppDelegateM: UIResponder, UIApplicationDelegate { - @Default(.appBundleID) var appBundleID - - var dataCacheHolder: DataCacheHolder? - - func application( - _: UIApplication, - didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - print("App Started") - initialiseLocalConsole() - initialiseNukeUI() - initialiseRealm() - PhaseChangeActions().homeScreenQuickActions() - listPostsMoya(parameters: ListPostsParameters(type: "All", sort: "Active")) - return true - } - - func initialiseLocalConsole() { - _ = LCManager.shared - } - - func initialiseNukeUI() { - DataLoader.sharedUrlCache.diskCapacity = 0 - - let pipeline = ImagePipeline { - self.dataCacheHolder = DataCacheHolder(appBundleID: self.appBundleID) - $0.dataCache = self.dataCacheHolder?.dataCache - $0.dataCachePolicy = .storeAll - $0.isProgressiveDecodingEnabled = true - } - - ImagePipeline.shared = pipeline - } - - func initialiseRealm() { - print("Realm DB Path:") - print("\(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path)") - - let config = Realm.Configuration( - schemaVersion: 3, - migrationBlock: { _, oldSchemaVersion in - if oldSchemaVersion < 1 {} - }, - deleteRealmIfMigrationNeeded: true - ) - Realm.Configuration.defaultConfiguration = config - } - - func listPostsMoya(parameters: ListPostsParameters = ListPostsParameters()) { - lemmyProvider.request(.listPosts(parameters: parameters)) { result in - switch result { - case let .success(response): - do { - let decoder = JSONDecoder() - let postsModel = try decoder.decode(PostModel.self, from: response.data) - - print(postsModel.posts[0].post.name) - print(postsModel.posts[1].post.name) - print(postsModel.posts[2].post.name) - - } catch { - print("Error decoding posts: \(error)") - } - - case let .failure(error): - print("Network request failed: \(error)") - } - } - } -} diff --git a/Lunar/APIService/CreatePostPopoverViewM.swift b/Lunar/APIService/CreatePostPopoverViewM.swift deleted file mode 100644 index 5ccbfc72..00000000 --- a/Lunar/APIService/CreatePostPopoverViewM.swift +++ /dev/null @@ -1,271 +0,0 @@ -//// -//// CreatePostPopoverView.swift -//// Lunar -//// -//// Created by Mani on 21/08/2023. -//// -// -//import Defaults -//import SFSafeSymbols -//import SwiftUI -// -//struct CreatePostPopoverViewM: View { -// @Default(.activeAccount) var activeAccount -// -// @Environment(\.dismiss) var dismiss -// -// @State private var postBody: String = "" -// @State private var userInputBody: String = "" -// @State var postName: String = "" -// @State var userInputTitle: String = "" -// @State var postURL: String = "" -// @State var userInputURL: String = "" -// @State var photoPickerIsPresented = false -// @State var pickerResult: [UIImage] = [] -// @State var showingImagePopover: Bool = false -// @State var showingImageUploadResult: Bool = false -// @State var responseMessage: String = "" -// @State var fileToken: String = "" -// @State var deleteToken: String = "" -// -// @State var siteMetadata = SiteMetadataObject( -// title: nil, -// description: nil, -// image: nil -// ) -// -// @State var expandSiteMetadata: Bool = false -// @State var imageIsUploading: Bool = false -// -// var communityID: Int -// var communityName: String -// var communityActorID: String -// -// var submittable: Bool { -// !userInputTitle.isEmpty && communityID != 0 -// } -// -// let notificationHaptics = UINotificationFeedbackGenerator() -// -// var body: some View { -// List { -// createPostHeading -// if activeAccount.userID.isEmpty { -// notLoggedInWarning -// } else { -// postingToSection -// postingAsSection -// titleField -// URLField -// bodyField -// imageUploaderSection -// submitButtonSection -// } -// } -// .listStyle(.insetGrouped) -// .popover(isPresented: $photoPickerIsPresented) { -// PhotoPicker( -// pickerResult: $pickerResult, -// isPresented: $photoPickerIsPresented -// ) -// } -// .popover(isPresented: $showingImagePopover) { -// LocalImagePopoverView( -// showingImagePopover: $showingImagePopover, -// image: pickerResult[0] -// ) -// } -// .alert("Uploaded Image Response", isPresented: $showingImageUploadResult) { -// VStack { -// Text("responseMessage: \(responseMessage)") -// Text("fileToken: \(fileToken)") -// Text("deleteToken: \(deleteToken)") -// } -// -// Button("OK", role: .cancel) {} -// } -// .onChange(of: pickerResult) { _ in -// print(pickerResult) -// } -// } -// -// var notLoggedInWarning: some View { -// HStack { -// Spacer() -// VStack { -// Image(systemSymbol: .personCropCircleBadgeExclamationmarkFill) -// .resizable() -// .scaledToFit() -// .frame(width: 100) -// .padding(30) -// .symbolRenderingMode(.palette) -// .foregroundStyle(.yellow, .secondary) -// Text("Login to Create Post") -// .bold() -// .font(.title2) -// .foregroundStyle(.secondary) -// } -// Spacer() -// } -// .listRowSeparator(.hidden) -// .listRowBackground(Color.clear) -// } -// -// var postingToSection: some View { -// Section { -// VStack(alignment: .leading) { -// HStack(spacing: 1) { -// Text(communityName) -// .bold() -// Text("@\(URLParser.extractDomain(from: communityActorID))") -// .foregroundStyle(.secondary) -// } -// } -// } header: { -// Text("Posting to:") -// .textCase(.none) -// } -// .listRowSeparator(.hidden) -// .listRowBackground(Color.clear) -// } -// -// var postingAsSection: some View { -// Section { -// VStack(alignment: .leading) { -// HStack(spacing: 1) { -// Text(activeAccount.name) -// .bold() -// Text("@\(URLParser.extractDomain(from: activeAccount.actorID))") -// .foregroundStyle(.secondary) -// } -// } -// } header: { -// Text("Posting as:") -// .textCase(.none) -// } -// .listRowSeparator(.hidden) -// .listRowBackground(Color.clear) -// } -// -// var createPostHeading: some View { -// Section { -// HStack { -// Text("Create Post") -// .font(.title) -// .bold() -// Spacer() -// Button { -// dismiss() -// } label: { -// Image(systemSymbol: .xmarkCircleFill) -// .font(.largeTitle) -// .foregroundStyle(.secondary) -// .saturation(0) -// } -// } -// } -// .listRowBackground(Color.clear) -// } -// -// var titleField: some View { -// Section { -// TextField(text: $userInputTitle) { -// Text("required").italic() -// .foregroundStyle(.red.opacity(0.3)) -// } -// } header: { -// Text("Title:") -// .textCase(.none) -// } -// } -// -// var URLField: some View { -// Section { -// URLInputView(siteMetadata: $siteMetadata, userInputURL: $userInputURL) -// } header: { -// Text("URL:") -// .textCase(.none) -// } footer: { -// URLInputFooterView( -// siteMetadata: $siteMetadata, -// expandSiteMetadata: $expandSiteMetadata -// ) -// } -// .onDebouncedChange(of: $userInputURL, debounceFor: 1) { newValue in -// URLInputDebounceLogic(siteMetadata: $siteMetadata, userInputURL: $userInputURL, newValue: newValue).run() -// } -// } -// -// var bodyField: some View { -// Section { -// TextEditor(text: $userInputBody) -// .background(Color.clear) -// .font(.body) -// .frame(height: 150) -// } header: { -// Text("Body:") -// .textCase(.none) -// } -// } -// -// var imageUploaderSection: some View { -// Section { -// ImageUploaderView( -// photoPickerIsPresented: $photoPickerIsPresented, -// pickerResult: $pickerResult, -// showingImagePopover: $showingImagePopover, -// showingImageUploadResult: $showingImageUploadResult, -// imageIsUploading: $imageIsUploading, -// responseMessage: $responseMessage, -// fileToken: $fileToken, -// deleteToken: $deleteToken -// ) -// } header: { -// Text("Image Uploader Coming Soon:") -// .textCase(.none) -// } -// } -// -// var submitButtonSection: some View { -// Section { -// Button { -// postBody = userInputBody -// postName = userInputTitle -// postURL = userInputURL -// if !postURL.isEmpty { -// postURL = "https://\(postURL)" -// } -// if submittable { -// -// let createPostParameters = CreatePostParameters(name: postName, communityId: communityID, url: postURL, body: postBody, languageId: 0, auth: JWT().getJWTFromKeychain(actorID: activeAccount.actorID) ?? "") -// -// PostSender().createPost(parameters: createPostParameters) { response, postID in -// if response == "success" { -// notificationHaptics.notificationOccurred(.success) -// dismiss() -// print("CREATED NEW POST: id=\(postID)") -// } else { -// notificationHaptics.notificationOccurred(.error) -// print("ERROR SUBMITTING POST") -// } -// } -// } -// } label: { -// HStack { -// Spacer() -// Text(submittable ? "Post" : "Complete required fields to post") -// .foregroundStyle(submittable ? .blue : .secondary) -// Spacer() -// } -// } -// } -// } -//} -// -//#Preview { -// CreatePostPopoverView( -// communityID: 234_309, -// communityName: "API Testing Pls Ignore", -// communityActorID: "https://lemmy.world/c/api_testing_pls_ignore" -// ) -//} diff --git a/Lunar/APIService/ParameterStructures.swift b/Lunar/APIService/ParameterStructures.swift deleted file mode 100644 index 02a87328..00000000 --- a/Lunar/APIService/ParameterStructures.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// DataStructs.swift -// Lunar -// -// Created by Mani on 05/12/2023. -// - -import Foundation - -struct ListPostsParameters { - var type: String? - var sort: String? - var page: Int? - var limit: Int? - var communityId: Int? - var communityName: String? - var savedOnly: Bool? - var likedOnly: Bool? - var dislikedOnly: Bool? - var pageCursor: String? -} - -struct CreatePostParameters { - var name: String - var communityId: Int - var url: String? - var body: String? - var honeypot: String? - var nsfw: Bool? - var languageId: Int - var auth: String -} diff --git a/Lunar/APIService/PostSenderM.swift b/Lunar/APIService/PostSenderM.swift deleted file mode 100644 index ce7f742e..00000000 --- a/Lunar/APIService/PostSenderM.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// PostSender.swift -// Lunar -// -// Created by Mani on 16/08/2023. -// - -import Alamofire -import Defaults -import Foundation -import Pulse -import SwiftUI - -class PostSenderM: ObservableObject { - @Default(.activeAccount) var activeAccount - @Default(.appBundleID) var appBundleID - @Default(.networkInspectorEnabled) var networkInspectorEnabled - - let pulse = Pulse.LoggerStore.shared - - func createPost(parameters: CreatePostParameters, completion: @escaping (String?, Int) -> Void) { - lemmyProvider.request(.createPost(parameters: parameters)) { result in - switch result { - case let .success(response): - do { - print(String(data: response.data, encoding: .utf8) ?? "Unable to convert data to string") - let responseData = try JSONDecoder().decode(CreatePostResponseModel.self, from: response.data) - let postID = Int(responseData.post?.post.id ?? 0) - print(String(postID)) - completion("success", postID) - } catch { - print("Error decoding create post response: \(error)") - } - case let .failure(error): - print("Network request failed: \(error)") - } - } - } -} diff --git a/Lunar/Account Tab/MyUserView.swift b/Lunar/Account Tab/MyUserView.swift index 8bfc04f6..b41b3f06 100644 --- a/Lunar/Account Tab/MyUserView.swift +++ b/Lunar/Account Tab/MyUserView.swift @@ -9,12 +9,10 @@ import Defaults import Nuke import NukeUI import SFSafeSymbols -import Shiny import SwiftUI struct MyUserView: View { @Default(.activeAccount) var activeAccount - @Default(.iridescenceEnabled) var iridescenceEnabled @GestureState var dragAmount = CGSize.zero @@ -46,7 +44,7 @@ struct MyUserView: View { .listStyle(.insetGrouped) .onAppear { DispatchQueue.global(qos: .background).async { - if let jwt = JWT().getJWTFromKeychain(actorID: activeAccount.actorID) { + if let jwt = JWT().getJWT(actorID: activeAccount.actorID) { SiteInfoFetcher(jwt: jwt).fetchSiteInfo { _, _, _, _ in } } } @@ -224,9 +222,6 @@ struct MyUserView: View { } } .modifier(BlurredAndDisabledModifier(style: actorID.isEmpty ? .disabled : .none)) - .modifier( - ConditionalListRowBackgroundModifier( - background: iridescenceEnabled ? .iridescent : .defaultBackground)) } } diff --git a/Lunar/AppDelegate.swift b/Lunar/AppDelegate.swift index ea6eb3b3..088b3fcd 100644 --- a/Lunar/AppDelegate.swift +++ b/Lunar/AppDelegate.swift @@ -7,7 +7,6 @@ import Defaults import Foundation -import LocalConsole import Nuke import RealmSwift import SwiftUI @@ -22,17 +21,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { print("App Started") - initialiseLocalConsole() initialiseNukeUI() initialiseRealm() PhaseChangeActions().homeScreenQuickActions() return true } - func initialiseLocalConsole() { - _ = LCManager.shared - } - func initialiseNukeUI() { DataLoader.sharedUrlCache.diskCapacity = 0 diff --git a/Lunar/Comment View/NestedCommentsLogic.swift b/Lunar/Comment View/NestedCommentsLogic.swift index a28c2874..228a3c9a 100644 --- a/Lunar/Comment View/NestedCommentsLogic.swift +++ b/Lunar/Comment View/NestedCommentsLogic.swift @@ -22,7 +22,7 @@ extension CommentObject { } class NestedComment: ObservableObject { - let commentViewData: CommentObject + var commentViewData: CommentObject var subComments: [NestedComment] var indentLevel: Int diff --git a/Lunar/Comment View/RecursiveComment.swift b/Lunar/Comment View/RecursiveComment.swift index 58dbf4ed..9f1c1919 100644 --- a/Lunar/Comment View/RecursiveComment.swift +++ b/Lunar/Comment View/RecursiveComment.swift @@ -18,11 +18,17 @@ struct RecursiveComment: View { @State private var isExpanded = true @State var showCreateCommentPopover = false + @State var blockUserDialogPresented = false + @State var reportCommentSheetPresented = false + @State var reportReasonHolder: String = "" + @EnvironmentObject var commentsFetcher: CommentsFetcher let nestedComment: NestedComment let post: RealmPost + let notificationHaptics = UINotificationFeedbackGenerator() + let commentHierarchyColors: [Color] = [ .clear, .red, .orange, .yellow, .green, .cyan, .blue, .indigo, .purple, ] @@ -49,6 +55,20 @@ struct RecursiveComment: View { } .contextMenu { shareButton + Menu { + reportCommentButton + blockUserButton + } label: { + Label("More", systemSymbol: .ellipsisCircle) + } + } + .confirmationDialog("", isPresented: $blockUserDialogPresented) { + blockDialog + } message: { + Text("Block user \(URLParser.buildFullUsername(from: post.personActorID))") + } + .sheet(isPresented: $reportCommentSheetPresented) { + reportCommentSheet } ForEach(nestedComment.subComments, id: \.id) { subComment in @@ -65,6 +85,72 @@ struct RecursiveComment: View { } } + var reportCommentSheet: some View { + List { + Section { + HStack { + Text("Report Comment") + .font(.title) + .bold() + Spacer() + Button { + reportCommentSheetPresented = false + } label: { + Image(systemSymbol: .xmarkCircleFill) + .font(.largeTitle) + .foregroundStyle(.secondary) + .saturation(0) + } + } + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + + Section { + VStack(alignment: .leading) { + Text(URLParser.buildFullUsername(from: nestedComment.commentViewData.creator.actorID)) + .foregroundStyle(.secondary) + Text(nestedComment.commentViewData.comment.content) + .lineLimit(5) + .truncationMode(.tail) + } + } header: { + Text("Comment") + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + + Section { + TextEditor(text: $reportReasonHolder) + .background(Color.clear) + .font(.body) + .frame(height: 150) + } header: { + Text("Reason") + } + + Section { + Button { + let reportReason = reportReasonHolder + reportCommentAction(reportReason: reportReason) + } label: { + Text("Report") + } + .tint(.red) + .disabled(reportReasonHolder.isEmpty) + Button { + let reportReason = reportReasonHolder + reportCommentAction(reportReason: reportReason) + blockUserAction() + } label: { + Text("Report and Block User") + } + .tint(.red) + .disabled(reportReasonHolder.isEmpty) + } + } + } + var shareButton: some View { Button { let items: [Any] = [nestedComment.commentViewData.comment.content] @@ -74,6 +160,36 @@ struct RecursiveComment: View { } } + var blockUserButton: some View { + Button { + blockUserDialogPresented = true + } label: { + Label("Block User", systemSymbol: AllSymbols().blockContextIcon) + } + } + + var reportCommentButton: some View { + Button { + reportCommentSheetPresented = true + } label: { + Label("Report Comment", systemSymbol: AllSymbols().reportContextIcon) + } + } + + @ViewBuilder + var blockDialog: some View { + Button("Block User") { + blockUserAction() + } + Button("Report & Block") { + blockUserDialogPresented = false + reportCommentSheetPresented = true + } + Button("Dismiss", role: .cancel) { + blockUserDialogPresented = false + } + } + var minimisedCommentRow: some View { HStack { Text(nestedComment.commentViewData.comment.content) @@ -162,6 +278,7 @@ struct RecursiveComment: View { if commentMetadataPosition == "Top" { commentMetadata } + Markdown { nestedComment.commentViewData.comment.content } .markdownTextStyle(\.text) { FontSize(fontSize) } .markdownTheme(.gitHub) @@ -187,4 +304,30 @@ struct RecursiveComment: View { return count } + + func blockUserAction() { + if let personID = post.personID { + BlockSender(personID: personID, blockableObjectType: .person, block: true).blockUser { _, isBlockedResponse, _ in + if isBlockedResponse == true { + if let index = commentsFetcher.comments.firstIndex(where: { + $0.comment.id == nestedComment.commentViewData.comment.id + }) { + commentsFetcher.comments.remove(at: index) + } + } + } + } + } + + func reportCommentAction(reportReason: String) { + ReportSender(commentID: nestedComment.commentID, reportObjectType: .comment, reportReason: reportReason).sendReport { _, _, successful in + print(successful) + if successful == true { + notificationHaptics.notificationOccurred(.success) + reportCommentSheetPresented = false + } else { + notificationHaptics.notificationOccurred(.error) + } + } + } } diff --git a/Lunar/Common Views/AllSymbols.swift b/Lunar/Common Views/AllSymbols.swift index 2fd55f59..72707292 100644 --- a/Lunar/Common Views/AllSymbols.swift +++ b/Lunar/Common Views/AllSymbols.swift @@ -28,6 +28,18 @@ class AllSymbols { SFSafeSymbols.SFSymbol.slashCircle } + var blockCommunityIcon: SFSafeSymbols.SFSymbol { + SFSafeSymbols.SFSymbol.xmarkOctagonFill + } + + var blockContextIcon: SFSafeSymbols.SFSymbol { + SFSafeSymbols.SFSymbol.personCropCircleFillBadgeXmark + } + + var reportContextIcon: SFSafeSymbols.SFSymbol { + SFSafeSymbols.SFSymbol.exclamationmarkTriangleFill + } + var upvoteContextIcon: SFSafeSymbols.SFSymbol { SFSafeSymbols.SFSymbol.arrowUpCircle } @@ -141,7 +153,7 @@ class AllSymbols { Image(systemSymbol: .appDashed) .foregroundStyle(accentColorString == "Default" ? .purple : accentColor) } - + var christmasAppIconSettings: some View { Image(systemSymbol: .appGift) .foregroundStyle(accentColorString == "Default" ? .red : accentColor) diff --git a/Lunar/Common Views/JWT.swift b/Lunar/Common Views/JWT.swift deleted file mode 100644 index 6282558b..00000000 --- a/Lunar/Common Views/JWT.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// JWT.swift -// Lunar -// -// Created by Mani on 10/09/2023. -// - -import Defaults -import Foundation -import SwiftUI - -class JWT { - @Default(.appBundleID) var appBundleID - - func getJWTFromKeychain(actorID: String) -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } -} diff --git a/Lunar/DataLoaders/CommunitiesFetcher.swift b/Lunar/DataLoaders/CommunitiesFetcher.swift deleted file mode 100644 index bdd30f8a..00000000 --- a/Lunar/DataLoaders/CommunitiesFetcher.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// CommunitiesFetcher.swift -// Lunar -// -// Created by Mani on 09/07/2023. -// - -import Alamofire -import Defaults -import Foundation -import Nuke -import Pulse -import RealmSwift -import SwiftUI - -class CommunitiesFetcher: ObservableObject { - @Default(.communitiesSort) var communitiesSort - @Default(.communitiesType) var communitiesType - @Default(.activeAccount) var activeAccount - @Default(.appBundleID) var appBundleID - @Default(.networkInspectorEnabled) var networkInspectorEnabled - - let imagePrefetcher = ImagePrefetcher(pipeline: ImagePipeline.shared) - - private var currentPage = 1 - private var sortParameter: String? - private var typeParameter: String? - private var limitParameter: Int = 50 - private var communityID: Int? - private var jwt: String? - var instance: String? - - private var endpointPath: String { - if communityID != nil { - "/api/v3/community" - } else { - "/api/v3/community/list" - } - } - - private var endpoint: URLComponents { - URLBuilder( - endpointPath: endpointPath, - sortParameter: sortParameter, - typeParameter: typeParameter, - currentPage: currentPage, - limitParameter: limitParameter, - communityID: communityID, - jwt: jwt, - instance: instance - ).buildURL() - } - - private var endpointRedacted: URLComponents { - URLBuilder( - endpointPath: endpointPath, - sortParameter: sortParameter, - typeParameter: typeParameter, - currentPage: currentPage, - limitParameter: limitParameter, - communityID: communityID, - instance: instance - ).buildURL() - } - - let pulse = Pulse.LoggerStore.shared - - init( - limitParameter: Int, - sortParameter: String? = nil, - typeParameter: String? = nil, - instance: String? = nil - ) { - self.sortParameter = sortParameter ?? communitiesSort - self.typeParameter = typeParameter ?? communitiesType - self.limitParameter = limitParameter - - /// Force an instance if it's different to the one you want - self.instance = instance - - jwt = getJWTFromKeychain(actorID: activeAccount.actorID) ?? "" - } - - func loadContent(isRefreshing _: Bool = false) { - let cacher = ResponseCacher(behavior: .cache) - - var headers: HTTPHeaders = [] - if let jwt { - headers = [.authorization(bearerToken: jwt)] - } - - AF.request(endpoint, headers: headers) { urlRequest in - urlRequest.cachePolicy = .returnCacheDataElseLoad - } - .cacheResponse(using: cacher) - .validate(statusCode: 200 ..< 300) - .responseDecodable(of: CommunityModel.self) { response in - - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } - - switch response.result { - case let .success(result): - - print(result.communities.count) - - let imagesToPrefetch = result.iconURLs.compactMap { URL(string: $0) } - self.imagePrefetcher.startPrefetching(with: imagesToPrefetch) - - let realm = try! Realm() - - try! realm.write { - for community in result.communities { - let fetchedCommunity = RealmCommunity( - id: community.community.id, - name: community.community.name, - title: community.community.title, - actorID: community.community.actorID, - instanceID: community.community.instanceID, - descriptionText: community.community.description, - icon: community.community.icon, - banner: community.community.banner, - postingRestrictedToMods: community.community.postingRestrictedToMods, - published: community.community.published, - subscribers: community.counts.subscribers, - posts: community.counts.posts, - comments: community.counts.comments, - subscribed: community.subscribed - ) - realm.add(fetchedCommunity, update: .modified) - } - } - - case let .failure(error): - print("CommunitiesFetcher ERROR: \(error): \(error.errorDescription ?? "")") - } - } - } - - func getJWTFromKeychain(actorID: String) -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } -} diff --git a/Lunar/DataLoaders/PostSiteMetadataFetcher.swift b/Lunar/DataLoaders/PostSiteMetadataFetcher.swift deleted file mode 100644 index 1085e65a..00000000 --- a/Lunar/DataLoaders/PostSiteMetadataFetcher.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// PostSiteMetadataFetcher.swift -// Lunar -// -// Created by Mani on 30/07/2023. -// - -import Alamofire -import Defaults -import Foundation -import Pulse -import SwiftUI - -class PostSiteMetadataFetcher: ObservableObject { - @Default(.networkInspectorEnabled) var networkInspectorEnabled - - private var endpoint: URLComponents - let pulse = Pulse.LoggerStore.shared - - init(urlString: String) { - endpoint = URLBuilder(endpointPath: "/api/v3/post/site_metadata", urlString: urlString).buildURL() - } - - func fetchPostSiteMetadata(completion: @escaping (SiteMetadataObject?) -> Void) { - AF.request(endpoint) - .validate(statusCode: 200 ..< 300) - .responseDecodable(of: PostSiteMetadataResponseModel.self) { response in - - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpoint, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } - - switch response.result { - case let .success(result): - completion(result.metadata) - - case let .failure(error): - if let data = response.data, - let fetchError = try? JSONDecoder().decode(ErrorResponseModel.self, from: data) - { - print("PostSiteMetadataFetcher ERROR: \(fetchError.error)") - completion(nil) - } else { - print("PostSiteMetadataFetcher JSON DECODE ERROR: \(error): \(String(describing: error.errorDescription))") - completion(nil) - } - } - } - } -} diff --git a/Lunar/DataLoaders/PostsFetcher.swift b/Lunar/DataLoaders/PostsFetcher.swift deleted file mode 100644 index 3ec13e55..00000000 --- a/Lunar/DataLoaders/PostsFetcher.swift +++ /dev/null @@ -1,238 +0,0 @@ -// -// PostsFetcher.swift -// Lunar -// -// Created by Mani on 23/07/2023. -// - -import Alamofire -import Combine -import Defaults -import Nuke -import Pulse -import RealmSwift -import SwiftUI - -class PostsFetcher: ObservableObject { - @Default(.activeAccount) var activeAccount - @Default(.appBundleID) var appBundleID - @Default(.networkInspectorEnabled) var networkInspectorEnabled - @Default(.selectedInstance) var selectedInstance - - @Published var isLoading = false - - let pulse = Pulse.LoggerStore.shared - let imagePrefetcher = ImagePrefetcher(pipeline: ImagePipeline.shared) - - var sort: String - var type: String - var communityID: Int? - var personID: Int? - var instance: String? - var filterKey: String - var endpointPath: String - // var page: Int - - @State private var page: Int = 1 - - private var endpoint: URLComponents { - URLBuilder( - endpointPath: endpointPath, - sortParameter: sort, - typeParameter: type, - currentPage: page, - limitParameter: 50, - communityID: communityID, - personID: personID, - jwt: getJWTFromKeychain(), - instance: instance - ).buildURL() - } - - private var endpointRedacted: URLComponents { - URLBuilder( - endpointPath: endpointPath, - sortParameter: sort, - typeParameter: type, - currentPage: page, - limitParameter: 50, - communityID: communityID, - personID: personID, - instance: instance - ).buildURL() - } - - init( - sort: String, - type: String, - communityID: Int? = 0, - personID: Int? = 0, - instance: String? = nil, - page: Int, - filterKey: String - ) { - /// When getting user specific posts, need to use a different endpoint path. - if personID == nil || personID == 0 { - endpointPath = "/api/v3/post/list" - } else { - endpointPath = "/api/v3/user" - } - - self.page = page - - if communityID == 99_999_999_999_999 { // TODO: just a placeholder to prevent running when user posts - self.communityID = 0 - } - - /// Values that can be passed in explicitly. Reverts to default if not passed in. - self.sort = sort - self.type = type - - /// Force an instance if it's different to the one you want - self.instance = instance - - self.communityID = communityID - self.personID = personID - - self.filterKey = filterKey - } - - func loadContent(isRefreshing: Bool = false) { - guard !isLoading else { return } - - isLoading = true - - let cacher = ResponseCacher(behavior: .cache) - - var headers: HTTPHeaders = [] - if let jwt = getJWTFromKeychain() { - headers = [.authorization(bearerToken: jwt)] - } - - print("_____________FETCH_TRIGGERED_____________") - AF.request(endpoint, headers: headers) { urlRequest in - if isRefreshing { - urlRequest.cachePolicy = .reloadRevalidatingCacheData - } else { - urlRequest.cachePolicy = .returnCacheDataElseLoad - } - urlRequest.networkServiceType = .responsiveData - } - .cacheResponse(using: cacher) - .validate(statusCode: 200 ..< 300) - .responseDecodable(of: PostModel.self) { response in - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } - - switch response.result { - case let .success(result): - - /// When getting user specific posts, look for - - let imageRequestList = result.imageURLs.compactMap { - ImageRequest(url: URL(string: $0), processors: [.resize(width: 200)]) - } - self.imagePrefetcher.startPrefetching(with: imageRequestList) - - // MARK: - Realm - - let realm = try! Realm() - - let batchID = - "instance_\(self.instance ?? self.selectedInstance)" + "__sort_\(self.sort)" - + "__type_\(self.type)" + "__userUsed_\(Int(self.activeAccount.userID) ?? 0)" - + "__communityID_\(self.communityID ?? 0)" + "__personID_\(self.personID ?? 0)" - - let batch = Batch( - batchID: batchID, - instance: self.instance ?? self.selectedInstance, - sort: self.sort, - type: self.type, - numberOfPosts: 0, - page: self.page, - latestTime: NSDate().timeIntervalSince1970, - userUsed: Int(self.activeAccount.userID) ?? 0, - communityID: self.communityID ?? 0, - personID: self.personID ?? 0 - ) - - try! realm.write { - if let batch = realm.object(ofType: Batch.self, forPrimaryKey: batchID) { - print("batch found") - // batch.realmPosts.append(objectsIn: realmPosts) - batch.page = self.page - } else { - print("Batch not found with the primary key specified, creating new batch") - realm.add(batch, update: .modified) - // batch.realmPosts.append(objectsIn: realmPosts) - } - - for post in result.posts { - let fetchedPost = RealmPost( - postID: post.post.id, - postName: post.post.name, - postPublished: post.post.published, - postURL: post.post.url, - postBody: post.post.body, - postThumbnailURL: post.post.thumbnailURL, - postFeatured: post.post.featuredCommunity || post.post.featuredLocal, - personID: post.creator.id, - personName: post.creator.name, - personPublished: post.creator.published, - personActorID: post.creator.actorID, - personInstanceID: post.creator.instanceID, - personAvatar: post.creator.avatar, - personDisplayName: post.creator.displayName, - personBio: post.creator.bio, - personBanner: post.creator.banner, - communityID: post.community.id, - communityName: post.community.name, - communityTitle: post.community.title, - communityActorID: post.community.actorID, - communityInstanceID: post.community.instanceID, - communityDescription: post.community.description, - communityIcon: post.community.icon, - communityBanner: post.community.banner, - communityUpdated: post.community.updated, - communitySubscribed: post.subscribed, - postScore: post.counts.postScore, - postCommentCount: post.counts.comments, - upvotes: post.counts.upvotes, - downvotes: post.counts.downvotes, - postMyVote: post.myVote ?? 0, - postHidden: false, - postMinimised: post.post.featuredCommunity || post.post.featuredLocal, - sort: self.sort, - type: self.type, - filterKey: self.filterKey - ) - realm.add(fetchedPost, update: .modified) - // batch.realmPosts.append(fetchedPost) - } - } - self.isLoading = false - - case let .failure(error): - print("PostsFetcher ERROR: \(error): \(error.errorDescription ?? "")") - self.isLoading = false - } - } - } - - func getJWTFromKeychain() -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: activeAccount.actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } -} diff --git a/Lunar/DataLoaders/PrivateMessageFetcher.swift b/Lunar/DataLoaders/PrivateMessageFetcher.swift deleted file mode 100644 index 68e11bb3..00000000 --- a/Lunar/DataLoaders/PrivateMessageFetcher.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// PrivateMessageFetcher.swift -// Lunar -// -// Created by Mani on 22/11/2023. -// - -import Alamofire -import Combine -import Defaults -import Nuke -import Pulse -import RealmSwift -import SwiftUI - -class PrivateMessageFetcher: ObservableObject { - @Default(.activeAccount) var activeAccount - @Default(.appBundleID) var appBundleID - @Default(.networkInspectorEnabled) var networkInspectorEnabled - - @Published var isLoading = false - - let pulse = Pulse.LoggerStore.shared - var instance: String - var endpointPath: String = "/api/v3/private_message/list" - - private var endpoint: URLComponents { - URLBuilder( - endpointPath: endpointPath, - jwt: getJWTFromKeychain(), - instance: instance - ).buildURL() - } - - private var endpointRedacted: URLComponents { - URLBuilder( - endpointPath: endpointPath, - instance: instance - ).buildURL() - } - - init( - instance: String - ) { - self.instance = instance - } - - func loadContent(isRefreshing: Bool = false) { - guard !isLoading else { return } - - isLoading = true - - let cacher = ResponseCacher(behavior: .cache) - - var headers: HTTPHeaders = [] - if let jwt = getJWTFromKeychain() { - headers = [.authorization(bearerToken: jwt)] - } - - print("_____________FETCH_TRIGGERED_____________") - AF.request(endpoint, headers: headers) { urlRequest in - if isRefreshing { - urlRequest.cachePolicy = .reloadRevalidatingCacheData - } else { - urlRequest.cachePolicy = .returnCacheDataElseLoad - } - urlRequest.networkServiceType = .responsiveData - } - .cacheResponse(using: cacher) - .validate(statusCode: 200 ..< 300) - .responseDecodable(of: PrivateMessageModel.self) { response in - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } - - switch response.result { - case let .success(result): - - // MARK: - Realm - - let realm = try! Realm() - - try! realm.write { - for message in result.privateMessages { - let realmPrivateMessage = RealmPrivateMessage( - messageID: message.privateMessage.id, - messageContent: message.privateMessage.content, - messageDeleted: message.privateMessage.deleted, - messageRead: message.privateMessage.deleted, - messagePublished: message.privateMessage.published, - messageApID: message.privateMessage.apID, - messageIsLocal: message.privateMessage.local, - creatorID: message.creator.id ?? 0, - creatorName: message.creator.name, - creatorAvatar: message.creator.avatar ?? "", - creatorActorID: message.creator.actorID, - recipientID: message.recipient.id ?? 0, - recipientName: message.recipient.name, - recipientAvatar: message.recipient.avatar ?? "", - recipientActorID: message.recipient.actorID - ) - realm.add(realmPrivateMessage, update: .modified) - } - } - self.isLoading = false - - case let .failure(error): - print("PrivateMessageFetcher ERROR: \(error): \(error.errorDescription ?? "")") - self.isLoading = false - } - } - } - - func getJWTFromKeychain() -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: activeAccount.actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } -} diff --git a/Lunar/DataLoaders/SiteInfoFetcher.swift b/Lunar/DataLoaders/SiteInfoFetcher.swift deleted file mode 100644 index 4d9400e1..00000000 --- a/Lunar/DataLoaders/SiteInfoFetcher.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// SiteInfoFetcher.swift -// Lunar -// -// Created by Mani on 30/07/2023. -// - -import Alamofire -import Defaults -import Foundation -import Pulse -import SwiftUI - -class SiteInfoFetcher: ObservableObject { - @Default(.activeAccount) var activeAccount - @Default(.networkInspectorEnabled) var networkInspectorEnabled - @Default(.loggedInAccounts) var loggedInAccounts - - private var endpoint: URLComponents - private var endpointRedacted: URLComponents - private var jwt: String - - let widgetLink = WidgetLink() - - var loggedInAccount = AccountModel() - let pulse = Pulse.LoggerStore.shared - - init(jwt: String) { - self.jwt = jwt - endpoint = URLBuilder(endpointPath: "/api/v3/site", jwt: jwt).buildURL() - endpointRedacted = URLBuilder(endpointPath: "/api/v3/site").buildURL() - } - - func fetchSiteInfo(completion: @escaping (String?, String?, String?, String?) -> Void) { - let headers: HTTPHeaders = [.authorization(bearerToken: jwt)] - - AF.request(endpoint, headers: headers) - .validate(statusCode: 200 ..< 300) - .responseDecodable(of: SiteModel.self) { response in - - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } - - switch response.result { - case let .success(result): - - if let myUser = result.myUser { - let userID = myUser.localUserView.person.id - self.loggedInAccount.userID = String(userID ?? 0) - - let username = myUser.localUserView.person.name - self.loggedInAccount.name = username - - let email = myUser.localUserView.localUser.email - self.loggedInAccount.email = email ?? "" - - let avatarURL = myUser.localUserView.person.avatar - self.loggedInAccount.avatarURL = avatarURL ?? "" - - let actorID = myUser.localUserView.person.actorID - self.loggedInAccount.actorID = actorID - - let displayName = myUser.localUserView.person.displayName - self.loggedInAccount.displayName = displayName ?? username - - let postScore = myUser.localUserView.counts.postScore - self.loggedInAccount.postScore = postScore ?? 0 - - let commentScore = myUser.localUserView.counts.commentScore - self.loggedInAccount.commentScore = commentScore ?? 0 - - let postCount = myUser.localUserView.counts.postCount - self.loggedInAccount.postCount = postCount ?? 0 - - let commentCount = myUser.localUserView.counts.commentCount - self.loggedInAccount.commentCount = commentCount ?? 0 - let response = String(response.response?.statusCode ?? 0) - - var foundMatch = false - - for account in self.loggedInAccounts { - if actorID == account.actorID { - foundMatch = true - // completion(nil, nil, nil, nil) - break // Exit the loop early since a match was found - } - } - - // Example of loading an image based on a key - ImageDownloadManager(suiteName: "group.io.github.mani-sh-reddy.Lunar") - .loadImage(forKey: actorID.replacingOccurrences(of: "/", with: "_")) { _ in - } - - if let avatarURL { - self.storeUserAvatarToDisk( - avatarURL: avatarURL, - suiteName: "group.io.github.mani-sh-reddy.Lunar", - imageKey: actorID - .replacingOccurrences(of: "/", with: "_") - ) - } - - if !foundMatch { - self.loggedInAccounts.append(self.loggedInAccount) - self.activeAccount = self.loggedInAccount - self.widgetLink.storeAccountData(account: self.activeAccount) - self.widgetLink.reloadWidget(kind: "AccountWidget") - - completion(username, email, actorID, response) - } - } else { - completion(nil, nil, nil, "myUser not found") - print("Error getting myUser info from SiteInfoFetcher") - } - - /// This function would only trigger if login was a success, - /// so here you only really need to return api, internet, and json decode errors - case let .failure(error): - if let data = response.data, - let fetchError = try? JSONDecoder().decode(ErrorResponseModel.self, from: data) - { - print("fetchUsernameAndEmail ERROR: \(fetchError.error)") - completion(nil, nil, nil, fetchError.error) - } else { - print("fetchUsernameAndEmail JSON DECODE ERROR: \(error): \(String(describing: error.errorDescription))") - completion(nil, nil, nil, error.errorDescription) - } - } - } - } - - func storeUserAvatarToDisk(avatarURL: String, suiteName: String, imageKey: String) { - let imageDownloadManager = ImageDownloadManager(suiteName: suiteName) - if let imageURL = URL(string: avatarURL) { - imageDownloadManager.storeImage(fromURL: imageURL, forKey: imageKey) { success in - if success { - print("Image stored successfully. \(imageKey)") - } else { - print("Failed to store the image.") - } - } - } - } -} diff --git a/Lunar/DataModels/AccountModel.swift b/Lunar/DataModels/AccountModel.swift index 67a53003..1adbfd02 100644 --- a/Lunar/DataModels/AccountModel.swift +++ b/Lunar/DataModels/AccountModel.swift @@ -19,4 +19,5 @@ struct AccountModel: Codable, Hashable, Defaults.Serializable { var postCount: Int = 0 var commentScore: Int = 0 var commentCount: Int = 0 + var instance: String { URLParser.extractDomain(from: actorID) } } diff --git a/Lunar/DataModels/BlockResponseModel.swift b/Lunar/DataModels/BlockResponseModel.swift new file mode 100644 index 00000000..22191077 --- /dev/null +++ b/Lunar/DataModels/BlockResponseModel.swift @@ -0,0 +1,20 @@ +// +// BlockResponseModel.swift +// Lunar +// +// Created by Mani on 07/12/2023. +// + +import Foundation + +struct BlockResponseModel: Codable { + let person: PersonObject? + let community: CommunityObject? + let blocked: Bool + + enum CodingKeys: String, CodingKey { + case person = "person_view" + case community = "community_view" + case blocked + } +} diff --git a/Lunar/DataModels/Counts.swift b/Lunar/DataModels/Counts.swift index 27a23bb6..eb3637a3 100644 --- a/Lunar/DataModels/Counts.swift +++ b/Lunar/DataModels/Counts.swift @@ -8,7 +8,7 @@ import Foundation struct Counts: Codable { - let id: Int + let id: Int? let published: String? let posts: Int? diff --git a/Lunar/DataModels/ReportPostResponseModel.swift b/Lunar/DataModels/ReportPostResponseModel.swift new file mode 100644 index 00000000..de5f647e --- /dev/null +++ b/Lunar/DataModels/ReportPostResponseModel.swift @@ -0,0 +1,117 @@ +// +// ReportPostResponseModel.swift +// Lunar +// +// Created by Mani on 07/12/2023. +// + +import Foundation + +struct ReportResponseModel: Codable { + let postReportModel: PostReportModel? + let commentReportModel: CommentReportModel? + let privateMessageReportModel: PrivateMessageReportModel? + + enum CodingKeys: String, CodingKey { + case postReportModel = "post_report_view" + case commentReportModel = "comment_report_view" + case privateMessageReportModel = "private_message_report_view" + } +} + +struct PostReportModel: Codable { + let postReport: PostReportObject + let post: Post + let community: Community + let creator, postCreator: Person + let creatorBannedFromCommunity: Bool + let counts: Counts + + enum CodingKeys: String, CodingKey { + case postReport = "post_report" + case post, community, creator + case postCreator = "post_creator" + case creatorBannedFromCommunity = "creator_banned_from_community" + case counts + } +} + +struct CommentReportModel: Codable { + let commentReport: CommentReportObject + let comment: Comment + let post: Post + let community: Community + let creator, commentCreator: Person + let counts: Counts + let creatorBannedFromCommunity: Bool + + enum CodingKeys: String, CodingKey { + case commentReport = "comment_report" + case comment, post, community, creator + case commentCreator = "comment_creator" + case counts + case creatorBannedFromCommunity = "creator_banned_from_community" + } +} + +struct PrivateMessageReportModel: Codable { + let privateMessageReport: PrivateMessageReportObject + let privateMessage: PrivateMessage + let privateMessageCreator, creator: Person + + enum CodingKeys: String, CodingKey { + case privateMessageReport = "private_message_report" + case privateMessage = "private_message" + case privateMessageCreator = "private_message_creator" + case creator + } +} + +struct PostReportObject: Codable { + let id, creatorID, postID: Int + let originalPostName: String + let originalPostURL: String + let originalPostBody, reason: String + let resolved: Bool + let published: String + + enum CodingKeys: String, CodingKey { + case id + case creatorID = "creator_id" + case postID = "post_id" + case originalPostName = "original_post_name" + case originalPostURL = "original_post_url" + case originalPostBody = "original_post_body" + case reason, resolved, published + } +} + +struct CommentReportObject: Codable { + let id, creatorID, commentID: Int + let originalCommentText, reason: String + let resolved: Bool + let published: String + + enum CodingKeys: String, CodingKey { + case id + case creatorID = "creator_id" + case commentID = "comment_id" + case originalCommentText = "original_comment_text" + case reason, resolved, published + } +} + +struct PrivateMessageReportObject: Codable { + let id, creatorID, privateMessageID: Int + let originalPmText, reason: String + let resolved: Bool + let published: String + + enum CodingKeys: String, CodingKey { + case id + case creatorID = "creator_id" + case privateMessageID = "private_message_id" + case originalPmText = "original_pm_text" + case reason, resolved, published + } +} diff --git a/Lunar/Defaults/Defaults.swift b/Lunar/Defaults/Defaults.swift index e1f36093..48d64166 100644 --- a/Lunar/Defaults/Defaults.swift +++ b/Lunar/Defaults/Defaults.swift @@ -29,7 +29,6 @@ extension Defaults.Keys { static let detailedCommunityLabels = Key("detailedCommunityLabels", default: true) static let legacyPostsViewStyle = Key("legacyPostsViewStyle", default: "insetGrouped") static let postsViewStyle = Key("postsViewStyle", default: .large) - static let iridescenceEnabled = Key("iridescenceEnabled", default: false) static let prominentInspectorButton = Key("prominentInspectorButton", default: true) static let clearWhatsNewDefaults = Key("clearWhatsNewDefaults", default: false) static let clearInitialWhatsNewDefault = Key("clearInitialWhatsNewDefault", default: false) diff --git a/Lunar/Feed Views/CommunityRowView.swift b/Lunar/Feed Views/CommunityRowView.swift index 06321f1a..e670dd9c 100644 --- a/Lunar/Feed Views/CommunityRowView.swift +++ b/Lunar/Feed Views/CommunityRowView.swift @@ -17,33 +17,73 @@ struct CommunityRowView: View { @ObservedRealmObject var community: RealmCommunity + @State var blockCommunityDialogPresented = false + + @EnvironmentObject var communitiesFetcher: LegacyCommunitiesFetcher + var body: some View { - if detailedCommunityLabels { - HStack { - communityIconView + Group { + if detailedCommunityLabels { + HStack { + communityIconView - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .center, spacing: 4) { - communityNameView - communityLabelIcons + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center, spacing: 4) { + communityNameView + communityLabelIcons + } + communityCounts } - communityCounts + .padding(.horizontal, 10) + Spacer() + instanceLabel + } + } else { + HStack { + communityIconView + communityNameView } .padding(.horizontal, 10) - Spacer() - instanceLabel + .contextMenu { + unsubscribeContextMenuButton + } } - .contextMenu { + } + .contextMenu { + blockCommunityButton + if community.subscribed != .notSubscribed { unsubscribeContextMenuButton } - } else { - HStack { - communityIconView - communityNameView + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + blockCommunityButton + } + .confirmationDialog( + "Block community \(community.name)?", + isPresented: $blockCommunityDialogPresented, + actions: { + Button("Block", role: .destructive) { + blockCommunityAction() + } } - .padding(.horizontal, 10) - .contextMenu { - unsubscribeContextMenuButton + ) + } + + var blockCommunityButton: some View { + Button { + blockCommunityDialogPresented = true + } label: { + Label("Block Community", systemSymbol: AllSymbols().blockCommunityIcon) + } + .tint(.red) + } + + func blockCommunityAction() { + BlockSender(communityID: community.id, blockableObjectType: .community, block: true).blockUser { _, isBlockedResponse, _ in + if isBlockedResponse == true { + if let index = communitiesFetcher.communities.firstIndex(where: { $0.community.id == community.id }) { + communitiesFetcher.communities.remove(at: index) + } } } } diff --git a/Lunar/Feed Views/TrendingCommunitiesSectionView.swift b/Lunar/Feed Views/TrendingCommunitiesSectionView.swift index 9971402c..54e91ef3 100644 --- a/Lunar/Feed Views/TrendingCommunitiesSectionView.swift +++ b/Lunar/Feed Views/TrendingCommunitiesSectionView.swift @@ -39,10 +39,8 @@ struct TrendingCommunitiesSectionView: View { communityIcon: community.community.icon ) } label: { - LegacyCommunityRowView( - community: community, - communitiesFetcher: communitiesFetcher - ) + CommunityRowView(community: convertToRealmCommunity(community: community)) + .environmentObject(communitiesFetcher) } } .onChange(of: selectedInstance) { _ in @@ -54,4 +52,23 @@ struct TrendingCommunitiesSectionView: View { ProgressView() } } + + func convertToRealmCommunity(community: CommunityObject) -> RealmCommunity { + RealmCommunity( + id: community.community.id, + name: community.community.name, + title: community.community.title, + actorID: community.community.actorID, + instanceID: community.community.instanceID, + descriptionText: community.community.description, + icon: community.community.icon, + banner: community.community.banner, + postingRestrictedToMods: community.community.postingRestrictedToMods, + published: community.community.published, + subscribers: community.counts.subscribers, + posts: community.counts.posts, + comments: community.counts.comments, + subscribed: community.subscribed + ) + } } diff --git a/Lunar/Fetchers/BlockSender.swift b/Lunar/Fetchers/BlockSender.swift new file mode 100644 index 00000000..8896f3ed --- /dev/null +++ b/Lunar/Fetchers/BlockSender.swift @@ -0,0 +1,99 @@ +// +// BlockSender.swift +// Lunar +// +// Created by Mani on 07/12/2023. +// + +import Alamofire +import Defaults +import SwiftUI + +enum BlockableObjectType { + case person + case community +} + +class BlockSender: ObservableObject { + @Default(.activeAccount) var activeAccount + @Default(.appBundleID) var appBundleID + + private var personID: Int? + private var communityID: Int? + private var blockableObjectType: BlockableObjectType + private var block: Bool + + init( + personID: Int? = 0, + communityID: Int? = 0, + blockableObjectType: BlockableObjectType, + block: Bool + ) { + self.personID = personID + self.communityID = communityID + self.blockableObjectType = blockableObjectType + self.block = block + } + + func blockUser(completion: @escaping (Int?, Bool?, Bool?) -> Void) { + var JSONparameters = + [ + "block": block, + "auth": JWT().getJWTForActiveAccount() as Any, + ] as [String: Any] + + var endpoint = "" + + switch blockableObjectType { + case .person: + JSONparameters["person_id"] = personID + endpoint = "https://\(activeAccount.instance)/api/v3/user/block" + case .community: + JSONparameters["community_id"] = communityID + endpoint = "https://\(activeAccount.instance)/api/v3/community/block" + } + + AF.request( + endpoint, + method: .post, + parameters: JSONparameters, + encoding: JSONEncoding.default, + headers: GenerateHeaders().generate() + ) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: BlockResponseModel.self) { response in + + PulseWriter().write(response, EndpointParameters(endpointPath: endpoint), .post) + + print(response.request ?? "") + switch response.result { + case let .success(result): + print(result.blocked) + + var itemID: Int = 0 + + switch self.blockableObjectType { + case .person: + itemID = result.person?.person.id ?? 0 + case .community: + itemID = result.community?.community.id ?? 0 + } + + let isBlockedResponse = result.blocked + + completion(itemID, isBlockedResponse, true) + + case let .failure(error): + if let data = response.data, + let fetchError = try? JSONDecoder().decode(ErrorResponseModel.self, from: data) + { + print("BlockUserSender ERROR: \(fetchError.error)") + } else { + let errorDescription = String(describing: error.errorDescription) + print("BlockUserSender JSON DECODE ERROR: \(error): \(errorDescription)") + } + completion(nil, nil, false) + } + } + } +} diff --git a/Lunar/DataSenders/CommentSender.swift b/Lunar/Fetchers/CommentSender.swift similarity index 100% rename from Lunar/DataSenders/CommentSender.swift rename to Lunar/Fetchers/CommentSender.swift diff --git a/Lunar/DataLoaders/CommentsFetcher.swift b/Lunar/Fetchers/CommentsFetcher.swift similarity index 67% rename from Lunar/DataLoaders/CommentsFetcher.swift rename to Lunar/Fetchers/CommentsFetcher.swift index 712a7daf..29438c42 100644 --- a/Lunar/DataLoaders/CommentsFetcher.swift +++ b/Lunar/Fetchers/CommentsFetcher.swift @@ -6,10 +6,8 @@ // import Alamofire -import Combine import Defaults import Foundation -import Pulse import SwiftUI class CommentsFetcher: ObservableObject { @@ -27,8 +25,8 @@ class CommentsFetcher: ObservableObject { private var limitParameter: Int = 50 private let maxDepth: Int = 50 - private var endpoint: URLComponents { - URLBuilder( + private var parameters: EndpointParameters { + EndpointParameters( endpointPath: "/api/v3/comment/list", sortParameter: commentSort, typeParameter: commentType, @@ -36,24 +34,10 @@ class CommentsFetcher: ObservableObject { limitParameter: limitParameter, postID: postID, maxDepth: maxDepth, - jwt: getJWTFromKeychain() - ).buildURL() + jwt: JWT().getJWTForActiveAccount() + ) } - private var endpointRedacted: URLComponents { - URLBuilder( - endpointPath: "/api/v3/comment/list", - sortParameter: commentSort, - typeParameter: commentType, - currentPage: currentPage, - limitParameter: limitParameter, - postID: postID, - maxDepth: maxDepth - ).buildURL() - } - - let pulse = Pulse.LoggerStore.shared - init(postID: Int) { self.postID = postID loadContent() @@ -71,12 +55,10 @@ class CommentsFetcher: ObservableObject { let cacher = ResponseCacher(behavior: .cache) - var headers: HTTPHeaders = [] - if let jwt = getJWTFromKeychain() { - headers = [.authorization(bearerToken: jwt)] - } - - AF.request(endpoint, headers: headers) { urlRequest in + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) { urlRequest in if isRefreshing { urlRequest.cachePolicy = .reloadRevalidatingCacheData } else { @@ -88,14 +70,7 @@ class CommentsFetcher: ObservableObject { .validate(statusCode: 200 ..< 300) .responseDecodable(of: CommentModel.self) { response in - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } + PulseWriter().write(response, self.parameters, .get) switch response.result { case let .success(result): @@ -130,17 +105,6 @@ class CommentsFetcher: ObservableObject { } } - func getJWTFromKeychain() -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: activeAccount.actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } - func updateCommentCollapseState(_ comment: CommentObject, isCollapsed: Bool) { if let index = comments.firstIndex(where: { $0.comment.id == comment.comment.id }) { comments[index].isCollapsed = isCollapsed diff --git a/Lunar/Fetchers/CommunitiesFetcher.swift b/Lunar/Fetchers/CommunitiesFetcher.swift new file mode 100644 index 00000000..699658cb --- /dev/null +++ b/Lunar/Fetchers/CommunitiesFetcher.swift @@ -0,0 +1,97 @@ +// +// CommunitiesFetcher.swift +// Lunar +// +// Created by Mani on 09/07/2023. +// + +import Alamofire +import Defaults +import Foundation +import Nuke +import Pulse +import RealmSwift +import SwiftUI + +class CommunitiesFetcher: ObservableObject { + @Default(.communitiesSort) var communitiesSort + @Default(.communitiesType) var communitiesType + @Default(.activeAccount) var activeAccount + @Default(.appBundleID) var appBundleID + @Default(.networkInspectorEnabled) var networkInspectorEnabled + + let imagePrefetcher = ImagePrefetcher(pipeline: ImagePipeline.shared) + + private var currentPage = 1 + private var sortParameter: String? + private var typeParameter: String? + private var limitParameter: Int = 50 + private var communityID: Int? + var instance: String? + + private var endpointPath: String { + if communityID != nil { + "/api/v3/community" + } else { + "/api/v3/community/list" + } + } + + private var parameters: EndpointParameters { + EndpointParameters( + endpointPath: endpointPath, + sortParameter: sortParameter, + typeParameter: typeParameter, + currentPage: currentPage, + limitParameter: limitParameter, + communityID: communityID, + jwt: JWT().getJWTForActiveAccount(), + instance: instance + ) + } + + init( + limitParameter: Int, + sortParameter: String? = nil, + typeParameter: String? = nil, + instance: String? = nil + ) { + self.sortParameter = sortParameter ?? communitiesSort + self.typeParameter = typeParameter ?? communitiesType + self.limitParameter = limitParameter + + /// Force an instance if it's different to the one you want + self.instance = instance + } + + func loadContent(isRefreshing _: Bool = false) { + let cacher = ResponseCacher(behavior: .cache) + + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) { urlRequest in + urlRequest.cachePolicy = .returnCacheDataElseLoad + } + .cacheResponse(using: cacher) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: CommunityModel.self) { response in + + PulseWriter().write(response, self.parameters, .get) + + switch response.result { + case let .success(result): + + print(result.communities.count) + + let imagesToPrefetch = result.iconURLs.compactMap { URL(string: $0) } + self.imagePrefetcher.startPrefetching(with: imagesToPrefetch) + + RealmWriter().writeCommunity(communities: result.communities) + + case let .failure(error): + print("CommunitiesFetcher ERROR: \(error): \(error.errorDescription ?? "")") + } + } + } +} diff --git a/Lunar/Fetchers/EndpointBuilder.swift b/Lunar/Fetchers/EndpointBuilder.swift new file mode 100644 index 00000000..d5f18c27 --- /dev/null +++ b/Lunar/Fetchers/EndpointBuilder.swift @@ -0,0 +1,88 @@ +// +// EndpointBuilder.swift +// Lunar +// +// Created by Mani on 23/07/2023. +// + +import Defaults +import Foundation +import SwiftUI + +/// **Base URL** +/// https://lemmy.world/api/v3/ +/// **Communities List** +/// community/list?type_=Local&sort=Active&page=1&limit=10 +/// **Community Specific Posts** +/// post/list?type_=Local&sort=Active&page=1&limit=10&community_id=145566 +/// **Non Specific Posts** +/// post/list?type_=Local&sort=Active&page=1&limit=10 +/// **Comments List** +/// comment/list?type_=Local&sort=Top&max_depth=2&page=1&limit=10&post_id=2021423 + +struct EndpointParameters { + var endpointPath: String + var sortParameter: String? + var typeParameter: String? + var currentPage: Int? + var limitParameter: Int? + var savedOnly: Bool? + var communityID: Int? + var personID: Int? + var postID: Int? + var maxDepth: Int? + var jwt: String? + var searchQuery: String? + var listingType: String? + var voteType: Int? + var instance: String? + var urlString: String? +} + +class EndpointBuilder { + // If selected instance doesn't match active user, item IDs won't match and user will not be able to perform actions + @Default(.selectedInstance) var selectedInstance + + private let parameters: EndpointParameters + + init(parameters: EndpointParameters) { + self.parameters = parameters + } + + func build(redact: Bool? = false) -> URLComponents { + var endpoint = URLComponents() + var queryParams: [String: String?] = [:] + + if let sortParameter = parameters.sortParameter { queryParams["sort"] = String(sortParameter) } + if let typeParameter = parameters.typeParameter { queryParams["type_"] = String(typeParameter) } + if let currentPage = parameters.currentPage { queryParams["page"] = String(currentPage) } + if let limitParameter = parameters.limitParameter { queryParams["limit"] = String(limitParameter) } + if let savedOnly = parameters.savedOnly { queryParams["saved_only"] = String(savedOnly) } + if let communityID = parameters.communityID, communityID != 0 { queryParams["community_id"] = String(communityID) } + if let personID = parameters.personID, personID != 0 { queryParams["person_id"] = String(personID) } + if let postID = parameters.postID { queryParams["post_id"] = String(postID) } + if let maxDepth = parameters.maxDepth { queryParams["max_depth"] = String(maxDepth) } + + if redact == false { + if let jwt = parameters.jwt { queryParams["auth"] = jwt } + } + + if let searchQuery = parameters.searchQuery { queryParams["q"] = String(searchQuery) } + if let listingType = parameters.listingType { queryParams["listing_type"] = String(listingType) } + if let voteType = parameters.voteType { queryParams["score"] = String(voteType) } + if let urlString = parameters.urlString { queryParams["url"] = String(urlString) } + + endpoint.scheme = "https" + + if let instance = parameters.instance { + endpoint.host = instance + } else { + endpoint.host = selectedInstance + } + + endpoint.path = parameters.endpointPath + endpoint.setQueryItems(with: queryParams) + + return endpoint + } +} diff --git a/Lunar/Fetchers/GenerateHeaders.swift b/Lunar/Fetchers/GenerateHeaders.swift new file mode 100644 index 00000000..670035b7 --- /dev/null +++ b/Lunar/Fetchers/GenerateHeaders.swift @@ -0,0 +1,22 @@ +// +// GenerateHeaders.swift +// Lunar +// +// Created by Mani on 06/12/2023. +// + +import Alamofire +import Defaults +import Foundation +import Pulse +import SwiftUI + +class GenerateHeaders { + func generate(jwt: String? = JWT().getJWTForActiveAccount()) -> HTTPHeaders? { + var headers: HTTPHeaders = [] + if let jwt { + headers.add(.authorization(bearerToken: jwt)) + } + return headers + } +} diff --git a/Lunar/DataSenders/ImageSender.swift b/Lunar/Fetchers/ImageSender.swift similarity index 100% rename from Lunar/DataSenders/ImageSender.swift rename to Lunar/Fetchers/ImageSender.swift diff --git a/Lunar/Fetchers/JWT.swift b/Lunar/Fetchers/JWT.swift new file mode 100644 index 00000000..556ab0a8 --- /dev/null +++ b/Lunar/Fetchers/JWT.swift @@ -0,0 +1,37 @@ +// +// JWT.swift +// Lunar +// +// Created by Mani on 06/12/2023. +// + +import Defaults +import Foundation +import SwiftUI + +class JWT { + @Default(.appBundleID) var appBundleID + @Default(.activeAccount) var activeAccount + + func getJWTForActiveAccount() -> String? { + if let keychainObject = KeychainHelper.standard.read( + service: appBundleID, account: activeAccount.actorID + ) { + let jwt = String(data: keychainObject, encoding: .utf8) ?? "" + return jwt.replacingOccurrences(of: "\"", with: "") + } else { + return nil + } + } + + func getJWT(actorID: String) -> String? { + if let keychainObject = KeychainHelper.standard.read( + service: appBundleID, account: actorID + ) { + let jwt = String(data: keychainObject, encoding: .utf8) ?? "" + return jwt.replacingOccurrences(of: "\"", with: "") + } else { + return nil + } + } +} diff --git a/Lunar/DataLoaders/PersonFetcher.swift b/Lunar/Fetchers/PersonFetcher.swift similarity index 64% rename from Lunar/DataLoaders/PersonFetcher.swift rename to Lunar/Fetchers/PersonFetcher.swift index eba36898..94060acb 100644 --- a/Lunar/DataLoaders/PersonFetcher.swift +++ b/Lunar/Fetchers/PersonFetcher.swift @@ -6,10 +6,8 @@ // import Alamofire -import Combine import Defaults import Nuke -import Pulse import SwiftUI class PersonFetcher: ObservableObject { @@ -17,12 +15,10 @@ class PersonFetcher: ObservableObject { @Default(.activeAccount) var activeAccount @Default(.networkInspectorEnabled) var networkInspectorEnabled -// @Published var personModel = [PersonModel]() @Published var posts = [PostObject]() @Published var comments = [CommentObject]() @Published var isLoading = false - let pulse = Pulse.LoggerStore.shared let imagePrefetcher = ImagePrefetcher(pipeline: ImagePipeline.shared) private var currentPage = 1 @@ -32,8 +28,8 @@ class PersonFetcher: ObservableObject { private var personID: Int private var instance: String? - private var endpoint: URLComponents { - URLBuilder( + private var parameters: EndpointParameters { + EndpointParameters( endpointPath: "/api/v3/user", sortParameter: sortParameter, typeParameter: typeParameter, @@ -41,22 +37,9 @@ class PersonFetcher: ObservableObject { limitParameter: 50, savedOnly: savedOnly, personID: personID, - jwt: getJWTFromKeychain(), + jwt: JWT().getJWTForActiveAccount(), instance: instance - ).buildURL() - } - - private var endpointRedacted: URLComponents { - URLBuilder( - endpointPath: "/api/v3/user", - sortParameter: sortParameter, - typeParameter: typeParameter, - currentPage: currentPage, - limitParameter: 50, - savedOnly: savedOnly, - personID: personID, - instance: instance - ).buildURL() + ) } init( @@ -78,13 +61,6 @@ class PersonFetcher: ObservableObject { loadContent() } -// func loadMoreContentIfNeeded(currentItem: PostObject) { -// guard currentItem.post.id == personModel.first?.posts.last?.post.id else { -// return -// } -// loadContent() -// } - func loadContent(isRefreshing: Bool = false) { guard !isLoading else { return } @@ -96,12 +72,10 @@ class PersonFetcher: ObservableObject { let cacher = ResponseCacher(behavior: .cache) - var headers: HTTPHeaders = [] - if let jwt = getJWTFromKeychain() { - headers = [.authorization(bearerToken: jwt)] - } - - AF.request(endpoint, headers: headers) { urlRequest in + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) { urlRequest in if isRefreshing { urlRequest.cachePolicy = .reloadRevalidatingCacheData } else { @@ -112,30 +86,19 @@ class PersonFetcher: ObservableObject { .cacheResponse(using: cacher) .validate(statusCode: 200 ..< 300) .responseDecodable(of: PersonModel.self) { response in - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } + PulseWriter().write(response, self.parameters, .get) switch response.result { case let .success(result): let fetchedPosts = result.posts let fetchedComments = result.comments -// self.personModel = [result] let imageRequestList = result.imageURLs.compactMap { ImageRequest(url: URL(string: $0), processors: [.resize(width: 200)]) } self.imagePrefetcher.startPrefetching(with: imageRequestList) -// let imagesToPrefetch = result.imageURLs.compactMap { URL(string: $0) } -// self.imagePrefetcher.startPrefetching(with: imagesToPrefetch) - if isRefreshing { self.posts = fetchedPosts self.comments = fetchedComments @@ -162,15 +125,4 @@ class PersonFetcher: ObservableObject { } } } - - func getJWTFromKeychain() -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: activeAccount.actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } } diff --git a/Lunar/DataSenders/PostSender.swift b/Lunar/Fetchers/PostSender.swift similarity index 100% rename from Lunar/DataSenders/PostSender.swift rename to Lunar/Fetchers/PostSender.swift diff --git a/Lunar/Fetchers/PostSiteMetadataFetcher.swift b/Lunar/Fetchers/PostSiteMetadataFetcher.swift new file mode 100644 index 00000000..e997d6d8 --- /dev/null +++ b/Lunar/Fetchers/PostSiteMetadataFetcher.swift @@ -0,0 +1,57 @@ +// +// PostSiteMetadataFetcher.swift +// Lunar +// +// Created by Mani on 30/07/2023. +// + +import Alamofire +import Defaults +import Foundation +import Pulse +import SwiftUI + +class PostSiteMetadataFetcher: ObservableObject { + @Default(.networkInspectorEnabled) var networkInspectorEnabled + + private var urlString: String + + private var parameters: EndpointParameters { + EndpointParameters( + endpointPath: "/api/v3/post/site_metadata", + urlString: urlString + ) + } + + init(urlString: String) { + self.urlString = urlString + } + + func fetchPostSiteMetadata(completion: @escaping (SiteMetadataObject?) -> Void) { + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: PostSiteMetadataResponseModel.self) { response in + + PulseWriter().write(response, self.parameters, .get) + + switch response.result { + case let .success(result): + completion(result.metadata) + + case let .failure(error): + if let data = response.data, + let fetchError = try? JSONDecoder().decode(ErrorResponseModel.self, from: data) + { + print("PostSiteMetadataFetcher ERROR: \(fetchError.error)") + completion(nil) + } else { + print("PostSiteMetadataFetcher JSON DECODE ERROR: \(error): \(String(describing: error.errorDescription))") + completion(nil) + } + } + } + } +} diff --git a/Lunar/Fetchers/PostsFetcher.swift b/Lunar/Fetchers/PostsFetcher.swift new file mode 100644 index 00000000..eb5e0b6f --- /dev/null +++ b/Lunar/Fetchers/PostsFetcher.swift @@ -0,0 +1,118 @@ +// +// PostsFetcher.swift +// Lunar +// +// Created by Mani on 23/07/2023. +// + +import Alamofire +import Defaults +import Nuke +import SwiftUI + +class PostsFetcher: ObservableObject { + @Default(.activeAccount) var activeAccount + @Default(.selectedInstance) var selectedInstance + + @Published var isLoading = false + + let imagePrefetcher = ImagePrefetcher(pipeline: ImagePipeline.shared) + + var sort: String + var type: String + var communityID: Int? + var personID: Int? + var instance: String? + var filterKey: String + var endpointPath: String + // var page: Int + + @State private var page: Int = 1 + + private var parameters: EndpointParameters { + EndpointParameters( + endpointPath: endpointPath, + sortParameter: sort, + typeParameter: type, + currentPage: page, + limitParameter: 50, + communityID: communityID, + personID: personID, + jwt: JWT().getJWTForActiveAccount(), + instance: instance + ) + } + + init( + sort: String, + type: String, + communityID: Int? = 0, + personID: Int? = 0, + instance: String? = nil, + page: Int, + filterKey: String + ) { + /// When getting user specific posts, need to use a different endpoint path. + if personID == nil || personID == 0 { + endpointPath = "/api/v3/post/list" + } else { + endpointPath = "/api/v3/user" + } + + self.page = page + + if communityID == 99_999_999_999_999 { // TODO: just a placeholder to prevent running when user posts + self.communityID = 0 + } + + /// Values that can be passed in explicitly. Reverts to default if not passed in. + self.sort = sort + self.type = type + /// Force an instance if it's different to the one you want + self.instance = instance + self.communityID = communityID + self.personID = personID + self.filterKey = filterKey + } + + func loadContent(isRefreshing _: Bool = false) { + guard !isLoading else { return } + + isLoading = true + + let cacher = ResponseCacher(behavior: .cache) + + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) + .cacheResponse(using: cacher) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: PostModel.self) { response in + + PulseWriter().write(response, self.parameters, .get) + + switch response.result { + case let .success(result): + + let imageRequestList = result.imageURLs.compactMap { + ImageRequest(url: URL(string: $0), processors: [.resize(width: 200)]) + } + self.imagePrefetcher.startPrefetching(with: imageRequestList) + + RealmWriter().writePost( + posts: result.posts, + sort: self.sort, + type: self.type, + filterKey: self.filterKey + ) + + self.isLoading = false + + case let .failure(error): + print("PostsFetcher ERROR: \(error): \(error.errorDescription ?? "")") + self.isLoading = false + } + } + } +} diff --git a/Lunar/Fetchers/PrivateMessageFetcher.swift b/Lunar/Fetchers/PrivateMessageFetcher.swift new file mode 100644 index 00000000..8919b6a2 --- /dev/null +++ b/Lunar/Fetchers/PrivateMessageFetcher.swift @@ -0,0 +1,74 @@ +// +// PrivateMessageFetcher.swift +// Lunar +// +// Created by Mani on 22/11/2023. +// + +import Alamofire +import Combine +import Defaults +import Nuke +import Pulse +import RealmSwift +import SwiftUI + +class PrivateMessageFetcher: ObservableObject { + @Default(.activeAccount) var activeAccount + @Default(.appBundleID) var appBundleID + @Default(.networkInspectorEnabled) var networkInspectorEnabled + + @Published var isLoading = false + + var instance: String + var endpointPath: String = "/api/v3/private_message/list" + + private var parameters: EndpointParameters { + EndpointParameters( + endpointPath: endpointPath, + jwt: JWT().getJWTForActiveAccount(), + instance: instance + ) + } + + init( + instance: String + ) { + self.instance = instance + } + + func loadContent(isRefreshing: Bool = false) { + guard !isLoading else { return } + + isLoading = true + + let cacher = ResponseCacher(behavior: .cache) + + AF.request(EndpointBuilder(parameters: parameters).build(), headers: GenerateHeaders().generate()) { urlRequest in + if isRefreshing { + urlRequest.cachePolicy = .reloadRevalidatingCacheData + } else { + urlRequest.cachePolicy = .returnCacheDataElseLoad + } + urlRequest.networkServiceType = .responsiveData + } + .cacheResponse(using: cacher) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: PrivateMessageModel.self) { response in + + PulseWriter().write(response, self.parameters, .get) + + switch response.result { + case let .success(result): + + RealmWriter().writePrivateMessage(privateMessages: result.privateMessages) + + self.isLoading = false + + case let .failure(error): + print("PrivateMessageFetcher ERROR: \(error): \(error.errorDescription ?? "")") + self.isLoading = false + } + } + } +} diff --git a/Lunar/Fetchers/PulseWriter.swift b/Lunar/Fetchers/PulseWriter.swift new file mode 100644 index 00000000..f130e4b4 --- /dev/null +++ b/Lunar/Fetchers/PulseWriter.swift @@ -0,0 +1,49 @@ +// +// PulseWriter.swift +// Lunar +// +// Created by Mani on 06/12/2023. +// + +import Alamofire +import Defaults +import Foundation +import Pulse +import SwiftUI + +class PulseWriter { + @Default(.networkInspectorEnabled) var networkInspectorEnabled + @Default(.selectedInstance) var selectedInstance + + let pulse = Pulse.LoggerStore.shared + + func write( + _ response: DataResponse, + _ parameters: EndpointParameters, + _ method: HTTPMethod + ) { + guard networkInspectorEnabled else { return } + + if method == .get { + pulse.storeRequest( + try! URLRequest( + url: EndpointBuilder(parameters: parameters).build(redact: true), + method: method + ), + response: response.response, + error: response.error, + data: response.data + ) + } else if method == .post { + pulse.storeRequest( + try! URLRequest( + url: URL(string: "https://\(selectedInstance)\(parameters.endpointPath)")!, + method: method + ), + response: response.response, + error: response.error, + data: response.data + ) + } + } +} diff --git a/Lunar/Fetchers/RealmWriter.swift b/Lunar/Fetchers/RealmWriter.swift new file mode 100644 index 00000000..a109e5b6 --- /dev/null +++ b/Lunar/Fetchers/RealmWriter.swift @@ -0,0 +1,123 @@ +// +// RealmWriter.swift +// Lunar +// +// Created by Mani on 06/12/2023. +// + +import RealmSwift +import SwiftUI + +class RealmWriter { + let realm = try! Realm() + + func writePost( + posts: [PostObject], + sort: String, + type: String, + filterKey: String + ) { + try! realm.write { + for post in posts { + guard !post.creatorBlocked else { return } + + let realmPost = RealmPost( + postID: post.post.id, + postName: post.post.name, + postPublished: post.post.published, + postURL: post.post.url, + postBody: post.post.body, + postThumbnailURL: post.post.thumbnailURL, + postFeatured: post.post.featuredCommunity || post.post.featuredLocal, + personID: post.creator.id, + personName: post.creator.name, + personPublished: post.creator.published, + personActorID: post.creator.actorID, + personInstanceID: post.creator.instanceID, + personAvatar: post.creator.avatar, + personDisplayName: post.creator.displayName, + personBio: post.creator.bio, + personBanner: post.creator.banner, + communityID: post.community.id, + communityName: post.community.name, + communityTitle: post.community.title, + communityActorID: post.community.actorID, + communityInstanceID: post.community.instanceID, + communityDescription: post.community.description, + communityIcon: post.community.icon, + communityBanner: post.community.banner, + communityUpdated: post.community.updated, + communitySubscribed: post.subscribed, + postScore: post.counts.postScore, + postCommentCount: post.counts.comments, + upvotes: post.counts.upvotes, + downvotes: post.counts.downvotes, + postMyVote: post.myVote ?? 0, + postHidden: false, + postMinimised: post.post.featuredCommunity || post.post.featuredLocal, + sort: sort, + type: type, + filterKey: filterKey + ) + realm.add(realmPost, update: .modified) + } + } + } + + func writePrivateMessage( + privateMessages: [PrivateMessageObject] + ) { + let realm = try! Realm() + + try! realm.write { + for message in privateMessages { + let realmPrivateMessage = RealmPrivateMessage( + messageID: message.privateMessage.id, + messageContent: message.privateMessage.content, + messageDeleted: message.privateMessage.deleted, + messageRead: message.privateMessage.deleted, + messagePublished: message.privateMessage.published, + messageApID: message.privateMessage.apID, + messageIsLocal: message.privateMessage.local, + creatorID: message.creator.id ?? 0, + creatorName: message.creator.name, + creatorAvatar: message.creator.avatar ?? "", + creatorActorID: message.creator.actorID, + recipientID: message.recipient.id ?? 0, + recipientName: message.recipient.name, + recipientAvatar: message.recipient.avatar ?? "", + recipientActorID: message.recipient.actorID + ) + realm.add(realmPrivateMessage, update: .modified) + } + } + } + + func writeCommunity( + communities: [CommunityObject] + ) { + let realm = try! Realm() + + try! realm.write { + for community in communities { + let fetchedCommunity = RealmCommunity( + id: community.community.id, + name: community.community.name, + title: community.community.title, + actorID: community.community.actorID, + instanceID: community.community.instanceID, + descriptionText: community.community.description, + icon: community.community.icon, + banner: community.community.banner, + postingRestrictedToMods: community.community.postingRestrictedToMods, + published: community.community.published, + subscribers: community.counts.subscribers, + posts: community.counts.posts, + comments: community.counts.comments, + subscribed: community.subscribed + ) + realm.add(fetchedCommunity, update: .modified) + } + } + } +} diff --git a/Lunar/Fetchers/ReportSender.swift b/Lunar/Fetchers/ReportSender.swift new file mode 100644 index 00000000..20385c4f --- /dev/null +++ b/Lunar/Fetchers/ReportSender.swift @@ -0,0 +1,111 @@ +// +// ReportSender.swift +// Lunar +// +// Created by Mani on 07/12/2023. +// + +import Alamofire +import Defaults +import SwiftUI + +enum ReportObjectType { + case post + case comment + case message +} + +class ReportSender: ObservableObject { + @Default(.activeAccount) var activeAccount + @Default(.appBundleID) var appBundleID + + private var postID: Int? + private var commentID: Int? + private var privateMessageID: Int? + private var reportObjectType: ReportObjectType + private var reportReason: String + + init( + postID: Int? = 0, + commentID: Int? = 0, + privateMessageID: Int? = 0, + reportObjectType: ReportObjectType, + reportReason: String + ) { + self.postID = postID + self.commentID = commentID + self.privateMessageID = privateMessageID + self.reportObjectType = reportObjectType + self.reportReason = reportReason + } + + func sendReport(completion: @escaping (Int?, Int?, Bool) -> Void) { + var JSONparameters = + [ + "reason": reportReason, + "auth": JWT().getJWTForActiveAccount() as Any, + ] as [String: Any] + + var endpoint = "" + + switch reportObjectType { + case .post: + JSONparameters["post_id"] = postID + endpoint = "https://\(activeAccount.instance)/api/v3/post/report" + case .comment: + JSONparameters["comment_id"] = commentID + endpoint = "https://\(activeAccount.instance)/api/v3/comment/report" + case .message: + JSONparameters["private_message_id"] = privateMessageID + endpoint = "https://\(activeAccount.instance)/api/v3/private_message/report" + } + + AF.request( + endpoint, + method: .post, + parameters: JSONparameters, + encoding: JSONEncoding.default, + headers: GenerateHeaders().generate() + ) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: ReportResponseModel.self) { response in + + PulseWriter().write(response, EndpointParameters(endpointPath: endpoint), .post) + + print(response.request ?? "") + switch response.result { + case let .success(result): + + var itemID: Int = 0 + var reportID: Int = 0 + + switch self.reportObjectType { + case .post: + itemID = result.postReportModel?.post.id ?? 0 + reportID = result.postReportModel?.postReport.id ?? 0 + case .comment: + itemID = result.commentReportModel?.comment.id ?? 0 + reportID = result.commentReportModel?.commentReport.commentID ?? 0 + case .message: + itemID = result.privateMessageReportModel?.privateMessage.id ?? 0 + reportID = result.privateMessageReportModel?.privateMessageReport.privateMessageID ?? 0 + } + + let _ = String(response.response?.statusCode ?? 0) + + completion(itemID, reportID, true) + + case let .failure(error): + if let data = response.data, + let fetchError = try? JSONDecoder().decode(ErrorResponseModel.self, from: data) + { + print("BlockUserSender ERROR: \(fetchError.error)") + } else { + let errorDescription = String(describing: error.errorDescription) + print("BlockUserSender JSON DECODE ERROR: \(error): \(errorDescription)") + } + completion(nil, nil, false) + } + } + } +} diff --git a/Lunar/DataLoaders/SearchFetcher.swift b/Lunar/Fetchers/SearchFetcher.swift similarity index 90% rename from Lunar/DataLoaders/SearchFetcher.swift rename to Lunar/Fetchers/SearchFetcher.swift index c66118bf..b6bb6871 100644 --- a/Lunar/DataLoaders/SearchFetcher.swift +++ b/Lunar/Fetchers/SearchFetcher.swift @@ -33,19 +33,17 @@ class SearchFetcher: ObservableObject { var sortParameter: String private var limitParameter: Int - private var endpoint: URLComponents { - URLBuilder( + private var parameters: EndpointParameters { + EndpointParameters( endpointPath: "/api/v3/search", sortParameter: sortParameter, typeParameter: typeParameter, currentPage: currentPage, limitParameter: limitParameter, searchQuery: searchQuery - ).buildURL() + ) } - let pulse = Pulse.LoggerStore.shared - init( searchQuery: String, sortParameter: String, @@ -82,7 +80,10 @@ class SearchFetcher: ObservableObject { let cacher = ResponseCacher(behavior: .cache) - AF.request(endpoint) { urlRequest in + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) { urlRequest in print("SearchFetcher LOAD \(urlRequest.url as Any)") urlRequest.cachePolicy = .returnCacheDataElseLoad } @@ -90,14 +91,7 @@ class SearchFetcher: ObservableObject { .validate(statusCode: 200 ..< 300) .responseDecodable(of: SearchModel.self) { response in - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpoint, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } + PulseWriter().write(response, self.parameters, .get) switch response.result { case let .success(result): diff --git a/Lunar/Fetchers/SiteInfoFetcher.swift b/Lunar/Fetchers/SiteInfoFetcher.swift new file mode 100644 index 00000000..adbfb61e --- /dev/null +++ b/Lunar/Fetchers/SiteInfoFetcher.swift @@ -0,0 +1,146 @@ +// +// SiteInfoFetcher.swift +// Lunar +// +// Created by Mani on 30/07/2023. +// + +import Alamofire +import Defaults +import Foundation +import Pulse +import SwiftUI + +class SiteInfoFetcher: ObservableObject { + @Default(.activeAccount) var activeAccount + @Default(.networkInspectorEnabled) var networkInspectorEnabled + @Default(.loggedInAccounts) var loggedInAccounts + + private var jwt: String + + let widgetLink = WidgetLink() + + var loggedInAccount = AccountModel() + + private var parameters: EndpointParameters { + EndpointParameters( + endpointPath: "/api/v3/site", + jwt: jwt + ) + } + + init(jwt: String) { + self.jwt = jwt + } + + func fetchSiteInfo(completion: @escaping (String?, String?, String?, String?) -> Void) { + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate(jwt: jwt) + ) + .validate(statusCode: 200 ..< 300) + .responseDecodable(of: SiteModel.self) { response in + + PulseWriter().write(response, self.parameters, .get) + + switch response.result { + case let .success(result): + + if let myUser = result.myUser { + let userID = myUser.localUserView.person.id + self.loggedInAccount.userID = String(userID ?? 0) + + let username = myUser.localUserView.person.name + self.loggedInAccount.name = username + + let email = myUser.localUserView.localUser.email + self.loggedInAccount.email = email ?? "" + + let avatarURL = myUser.localUserView.person.avatar + self.loggedInAccount.avatarURL = avatarURL ?? "" + + let actorID = myUser.localUserView.person.actorID + self.loggedInAccount.actorID = actorID + + let displayName = myUser.localUserView.person.displayName + self.loggedInAccount.displayName = displayName ?? username + + let postScore = myUser.localUserView.counts.postScore + self.loggedInAccount.postScore = postScore ?? 0 + + let commentScore = myUser.localUserView.counts.commentScore + self.loggedInAccount.commentScore = commentScore ?? 0 + + let postCount = myUser.localUserView.counts.postCount + self.loggedInAccount.postCount = postCount ?? 0 + + let commentCount = myUser.localUserView.counts.commentCount + self.loggedInAccount.commentCount = commentCount ?? 0 + let response = String(response.response?.statusCode ?? 0) + + var foundMatch = false + + for account in self.loggedInAccounts { + if actorID == account.actorID { + foundMatch = true + // completion(nil, nil, nil, nil) + break // Exit the loop early since a match was found + } + } + + // Example of loading an image based on a key + ImageDownloadManager(suiteName: "group.io.github.mani-sh-reddy.Lunar") + .loadImage(forKey: actorID.replacingOccurrences(of: "/", with: "_")) { _ in + } + + if let avatarURL { + self.storeUserAvatarToDisk( + avatarURL: avatarURL, + suiteName: "group.io.github.mani-sh-reddy.Lunar", + imageKey: actorID + .replacingOccurrences(of: "/", with: "_") + ) + } + + if !foundMatch { + self.loggedInAccounts.append(self.loggedInAccount) + self.activeAccount = self.loggedInAccount + self.widgetLink.storeAccountData(account: self.activeAccount) + self.widgetLink.reloadWidget(kind: "AccountWidget") + + completion(username, email, actorID, response) + } + } else { + completion(nil, nil, nil, "myUser not found") + print("Error getting myUser info from SiteInfoFetcher") + } + + /// This function would only trigger if login was a success, + /// so here you only really need to return api, internet, and json decode errors + case let .failure(error): + if let data = response.data, + let fetchError = try? JSONDecoder().decode(ErrorResponseModel.self, from: data) + { + print("fetchUsernameAndEmail ERROR: \(fetchError.error)") + completion(nil, nil, nil, fetchError.error) + } else { + print("fetchUsernameAndEmail JSON DECODE ERROR: \(error): \(String(describing: error.errorDescription))") + completion(nil, nil, nil, error.errorDescription) + } + } + } + } + + func storeUserAvatarToDisk(avatarURL: String, suiteName: String, imageKey: String) { + let imageDownloadManager = ImageDownloadManager(suiteName: suiteName) + if let imageURL = URL(string: avatarURL) { + imageDownloadManager.storeImage(fromURL: imageURL, forKey: imageKey) { success in + if success { + print("Image stored successfully. \(imageKey)") + } else { + print("Failed to store the image.") + } + } + } + } +} diff --git a/Lunar/DataSenders/SubscriptionActionSender.swift b/Lunar/Fetchers/SubscriptionActionSender.swift similarity index 89% rename from Lunar/DataSenders/SubscriptionActionSender.swift rename to Lunar/Fetchers/SubscriptionActionSender.swift index e3ee25b7..2b6a0aa1 100644 --- a/Lunar/DataSenders/SubscriptionActionSender.swift +++ b/Lunar/Fetchers/SubscriptionActionSender.swift @@ -20,8 +20,6 @@ class SubscriptionActionSender: ObservableObject { private var subscribeAction: Bool private var jwt: String = "" - let pulse = Pulse.LoggerStore.shared - init( communityID: Int, asActorID: String, @@ -57,14 +55,7 @@ class SubscriptionActionSender: ObservableObject { .validate(statusCode: 200 ..< 300) .responseDecodable(of: SubscribeResponseModel.self) { response in - if self.networkInspectorEnabled { - self.pulse.storeRequest( - response.request ?? URLRequest(url: URL(string: endpoint)!), - response: response.response, - error: response.error, - data: response.data - ) - } + PulseWriter().write(response, EndpointParameters(endpointPath: endpoint), .post) print(response.request ?? "") switch response.result { diff --git a/Lunar/DataSenders/VoteSender.swift b/Lunar/Fetchers/VoteSender.swift similarity index 100% rename from Lunar/DataSenders/VoteSender.swift rename to Lunar/Fetchers/VoteSender.swift diff --git a/Lunar/Legacy Views/LegacyCommunitiesFetcher.swift b/Lunar/Legacy Views/LegacyCommunitiesFetcher.swift index 9310c726..b80b41db 100644 --- a/Lunar/Legacy Views/LegacyCommunitiesFetcher.swift +++ b/Lunar/Legacy Views/LegacyCommunitiesFetcher.swift @@ -30,7 +30,6 @@ class LegacyCommunitiesFetcher: ObservableObject { var typeParameter: String? var limitParameter: Int = 50 var communityID: Int? - private var jwt: String? private var endpointPath: String { if communityID != nil { @@ -40,27 +39,16 @@ class LegacyCommunitiesFetcher: ObservableObject { } } - private var endpoint: URLComponents { - URLBuilder( + private var parameters: EndpointParameters { + EndpointParameters( endpointPath: endpointPath, sortParameter: sortParameter, typeParameter: typeParameter, currentPage: currentPage, limitParameter: limitParameter, communityID: communityID, - jwt: jwt - ).buildURL() - } - - private var endpointRedacted: URLComponents { - URLBuilder( - endpointPath: endpointPath, - sortParameter: sortParameter, - typeParameter: typeParameter, - currentPage: currentPage, - limitParameter: limitParameter, - communityID: communityID - ).buildURL() + jwt: JWT().getJWTForActiveAccount() + ) } let pulse = Pulse.LoggerStore.shared @@ -73,7 +61,6 @@ class LegacyCommunitiesFetcher: ObservableObject { self.sortParameter = sortParameter ?? communitiesSort self.typeParameter = typeParameter ?? communitiesType self.limitParameter = limitParameter - jwt = getJWTFromKeychain(actorID: activeAccount.actorID) ?? "" loadContent() } @@ -88,7 +75,10 @@ class LegacyCommunitiesFetcher: ObservableObject { let cacher = ResponseCacher(behavior: .cache) - AF.request(endpoint) { urlRequest in + AF.request( + EndpointBuilder(parameters: parameters).build(), + headers: GenerateHeaders().generate() + ) { urlRequest in if isRefreshing { urlRequest.cachePolicy = .reloadRevalidatingCacheData } else { @@ -99,16 +89,7 @@ class LegacyCommunitiesFetcher: ObservableObject { .validate(statusCode: 200 ..< 300) .responseDecodable(of: CommunityModel.self) { response in -// print("====== Fetched Communities ======") - - if self.networkInspectorEnabled { - self.pulse.storeRequest( - try! URLRequest(url: self.endpointRedacted, method: .get), - response: response.response, - error: response.error, - data: response.data - ) - } + PulseWriter().write(response, self.parameters, .get) switch response.result { case let .success(result): @@ -142,15 +123,4 @@ class LegacyCommunitiesFetcher: ObservableObject { } } } - - func getJWTFromKeychain(actorID: String) -> String? { - if let keychainObject = KeychainHelper.standard.read( - service: appBundleID, account: actorID - ) { - let jwt = String(data: keychainObject, encoding: .utf8) ?? "" - return jwt.replacingOccurrences(of: "\"", with: "") - } else { - return nil - } - } } diff --git a/Lunar/Login Views/LoginHelper.swift b/Lunar/Login Views/LoginHelper.swift index 80fe2e20..a2867563 100644 --- a/Lunar/Login Views/LoginHelper.swift +++ b/Lunar/Login Views/LoginHelper.swift @@ -85,7 +85,6 @@ class LoginHelper: ObservableObject { print("login successful inside handleLoginSuccess()") let jwt = fetchedData.jwt SiteInfoFetcher(jwt: jwt).fetchSiteInfo { _, _, actorID, _ in - if let validActorID = actorID { KeychainHelper.standard.save(jwt, service: self.appBundleID, account: validActorID) } diff --git a/Lunar/More Communities Views/ExploreCommunitiesView.swift b/Lunar/More Communities Views/ExploreCommunitiesView.swift index d8a53a55..98fa61f7 100644 --- a/Lunar/More Communities Views/ExploreCommunitiesView.swift +++ b/Lunar/More Communities Views/ExploreCommunitiesView.swift @@ -42,10 +42,8 @@ struct ExploreCommunitiesView: View { communityIcon: community.community.icon ) } label: { - LegacyCommunityRowView( - community: community, - communitiesFetcher: communitiesFetcher - ) + CommunityRowView(community: convertToRealmCommunity(community: community)) + .environmentObject(communitiesFetcher) } } } header: { @@ -88,6 +86,25 @@ struct ExploreCommunitiesView: View { communitiesFetcher.loadContent() } } + + func convertToRealmCommunity(community: CommunityObject) -> RealmCommunity { + RealmCommunity( + id: community.community.id, + name: community.community.name, + title: community.community.title, + actorID: community.community.actorID, + instanceID: community.community.instanceID, + descriptionText: community.community.description, + icon: community.community.icon, + banner: community.community.banner, + postingRestrictedToMods: community.community.postingRestrictedToMods, + published: community.community.published, + subscribers: community.counts.subscribers, + posts: community.counts.posts, + comments: community.counts.comments, + subscribed: community.subscribed + ) + } } #Preview { diff --git a/Lunar/Post Views/PostItem.swift b/Lunar/Post Views/PostItem.swift index 349ece5c..5e63a923 100644 --- a/Lunar/Post Views/PostItem.swift +++ b/Lunar/Post Views/PostItem.swift @@ -19,8 +19,14 @@ struct PostItem: View { @State var showSafari: Bool = false @State var subscribeAlertPresented: Bool = false + @State var reportPostSheetPresented: Bool = false + @State var blockUserDialogPresented: Bool = false + @State var reportReasonHolder: String = "" + + @Environment(\.dismiss) var dismissReportPostSheet let hapticsLight = UIImpactFeedbackGenerator(style: .light) + let notificationHaptics = UINotificationFeedbackGenerator() var image: String? { let thumbnail = post.postThumbnailURL ?? "" @@ -110,6 +116,116 @@ struct PostItem: View { hideButton minimiseButton shareButton + Menu { + reportPostButton + blockUserButton + } label: { + Label("More", systemSymbol: .ellipsisCircle) + } + } + .confirmationDialog("", isPresented: $blockUserDialogPresented) { + blockDialog + } message: { + Text("Block user \(URLParser.buildFullUsername(from: post.personActorID))") + } + .sheet(isPresented: $reportPostSheetPresented) { + reportSheet + } + } + + var reportSheet: some View { + List { + Section { + HStack { + Text("Report Post") + .font(.title) + .bold() + Spacer() + Button { + reportPostSheetPresented = false + } label: { + Image(systemSymbol: .xmarkCircleFill) + .font(.largeTitle) + .foregroundStyle(.secondary) + .saturation(0) + } + } + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + + Section { + VStack(alignment: .leading) { + Text(URLParser.buildFullUsername(from: post.personActorID)) + .foregroundStyle(.secondary) + Text(post.postName) + .lineLimit(5) + .truncationMode(.tail) + } + } header: { + Text("Post") + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + + Section { + TextEditor(text: $reportReasonHolder) + .background(Color.clear) + .font(.body) + .frame(height: 150) + } header: { + Text("Reason") + } + + Section { + Button { + let reportReason = reportReasonHolder + reportPostAction(reportReason: reportReason) + } label: { + Text("Report") + } + .tint(.red) + .disabled(reportReasonHolder.isEmpty) + Button { + let reportReason = reportReasonHolder + reportPostAction(reportReason: reportReason) + blockUserAction() + } label: { + Text("Report and Block User") + } + .tint(.red) + .disabled(reportReasonHolder.isEmpty) + } + } + } + + @ViewBuilder + var blockDialog: some View { + Button("Block User") { + blockUserAction() + } + Button("Report & Block") { + blockUserDialogPresented = false + reportPostSheetPresented = true + } + Button("Dismiss", role: .cancel) { + blockUserDialogPresented = false + } + } + + var reportPostButton: some View { + Button { + reportPostSheetPresented = true + } label: { + Label("Report Post", systemSymbol: AllSymbols().reportContextIcon) + } + } + + var blockUserButton: some View { + Button { + blockUserDialogPresented = true + } label: { + Label("Block User", systemSymbol: AllSymbols().blockContextIcon) } } @@ -385,4 +501,26 @@ struct PostItem: View { } } } + + func blockUserAction() { + if let personID = post.personID { + BlockSender(personID: personID, blockableObjectType: .person, block: true).blockUser { _, isBlockedResponse, _ in + if isBlockedResponse == true { + RealmThawFunctions().deleteAction(post: post) + } + } + } + } + + func reportPostAction(reportReason: String) { + ReportSender(postID: post.postID, reportObjectType: .post, reportReason: reportReason).sendReport { _, _, successful in + print(successful) + if successful == true { + notificationHaptics.notificationOccurred(.success) + reportPostSheetPresented = false + } else { + notificationHaptics.notificationOccurred(.error) + } + } + } } diff --git a/Lunar/Settings Views/ManageInstancesView.swift b/Lunar/Settings Views/ManageInstancesView.swift index 9b7e5506..5e91bcbb 100644 --- a/Lunar/Settings Views/ManageInstancesView.swift +++ b/Lunar/Settings Views/ManageInstancesView.swift @@ -24,16 +24,6 @@ struct ManageInstancesView: View { @State var showingResetConfirmation = false var body: some View { - /// **Future implementation** - // DroppableList("Users 1", users: $users1) { dropped, index in - // users1.insert(dropped, at: index) - // users2.removeAll { $0 == dropped } - // } - // DroppableList("Users 2", users: $users2) { dropped, index in - // users2.insert(dropped, at: index) - // users1.removeAll { $0 == dropped } - // } - List { if debugModeEnabled { Text(String(describing: lemmyInstances)) diff --git a/Lunar/Settings Views/SettingsAccountView.swift b/Lunar/Settings Views/SettingsAccountView.swift index 01e6a694..003d4e53 100644 --- a/Lunar/Settings Views/SettingsAccountView.swift +++ b/Lunar/Settings Views/SettingsAccountView.swift @@ -9,6 +9,9 @@ import Defaults import SwiftUI struct SettingsAccountView: View { + @Default(.activeAccount) var activeAccount + @Default(.selectedInstance) var selectedInstance + @State var showingPopover: Bool = false @State var isPresentingConfirm: Bool = false @State var logoutAllUsersButtonClicked: Bool = false @@ -21,6 +24,25 @@ struct SettingsAccountView: View { var body: some View { List { + if !activeAccount.actorID.isEmpty, selectedInstance != URLParser.extractDomain(from: activeAccount.actorID) { + Section { + VStack(spacing: 10) { + Text("Note: If the current user's home instance differs from the selected instance, errors may occur while attempting actions such as voting, replying, or blocking.") + HStack { + Spacer() + Image(systemSymbol: .exclamationmarkTriangleFill).symbolRenderingMode(.multicolor) + Text("\(URLParser.extractDomain(from: activeAccount.actorID)) x \(selectedInstance)") + .padding(.vertical, 10) + Spacer() + } + } + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .font(.caption) + .foregroundStyle(.gray) + } + Section { if isLoginFlowComplete { LoggedInUsersListView() diff --git a/Lunar/Settings Views/SettingsAppIconPickerView.swift b/Lunar/Settings Views/SettingsAppIconPickerView.swift index c04b7d97..1db517b7 100644 --- a/Lunar/Settings Views/SettingsAppIconPickerView.swift +++ b/Lunar/Settings Views/SettingsAppIconPickerView.swift @@ -17,23 +17,23 @@ struct SettingsAppIconPickerView: View { let notificationHaptics = UINotificationFeedbackGenerator() private var appIcons: OrderedDictionary { - return [ + [ "Default": [ - ("Default", "Default", "") + ("Default", "Default", ""), ], "Community Icons": [ - ("@MrSebSin", "MrSebSin", "https://lemmy.world/u/MrSebSin") + ("@MrSebSin", "MrSebSin", "https://lemmy.world/u/MrSebSin"), ], "Created by Mani": [ ("Lemming", "Lemming", ""), ("Kbin", "Kbin", ""), - ("Space", "Space", "") + ("Space", "Space", ""), ], "Colors": [ ("Blue", "v0", ""), ("Black", "Night", ""), - ("Purple", "Purple", "") - ] + ("Purple", "Purple", ""), + ], ] } diff --git a/Lunar/Settings Views/SettingsAppearanceView.swift b/Lunar/Settings Views/SettingsAppearanceView.swift index daedf239..e4001790 100644 --- a/Lunar/Settings Views/SettingsAppearanceView.swift +++ b/Lunar/Settings Views/SettingsAppearanceView.swift @@ -8,13 +8,16 @@ import Defaults import MarkdownUI import SFSafeSymbols -import Shiny import SwiftUI +enum PostsViewStyle: String, Defaults.Serializable { + case large + case compact +} + struct SettingsAppearanceView: View { @AppStorage("appAppearance") var appAppearance: AppearanceOptions = .system - @Default(.iridescenceEnabled) var iridescenceEnabled @Default(.accentColor) var accentColor @Default(.accentColorString) var accentColorString @Default(.commentMetadataPosition) var commentMetadataPosition @@ -40,7 +43,6 @@ struct SettingsAppearanceView: View { Section { appearancePicker accentColorPicker - iridescenceSection } header: { Text("Style") } Section { @@ -151,16 +153,6 @@ struct SettingsAppearanceView: View { } } } - - var iridescenceSection: some View { - Section { - Toggle(isOn: $iridescenceEnabled) { - Text("Iridescent Effects") - } - .tint(accentColorString == "Default" ? .indigo : accentColor) - } - .modifier(ConditionalListRowBackgroundModifier(background: iridescenceEnabled ? .iridescent : .defaultBackground)) - } } #Preview { diff --git a/Lunar/Settings Views/SettingsDevOptionsView.swift b/Lunar/Settings Views/SettingsDevOptionsView.swift index 4ec2fadd..3e16620d 100644 --- a/Lunar/Settings Views/SettingsDevOptionsView.swift +++ b/Lunar/Settings Views/SettingsDevOptionsView.swift @@ -6,7 +6,6 @@ // import Defaults -import LocalConsole import Pulse import PulseUI import SFSafeSymbols @@ -22,33 +21,12 @@ struct SettingsDevOptionsView: View { @Default(.accentColor) var accentColor @State var settingsViewOpacity: Double = 1 - @State private var localConsoleButtonText: String = "Enable Local Console" let notificationHaptics = UINotificationFeedbackGenerator() let haptics = UIImpactFeedbackGenerator(style: .soft) var body: some View { List { - // MARK: - LOCAL CONSOLE - - Section { - Button { - haptics.impactOccurred(intensity: 0.7) - LCManager.shared.isVisible.toggle() - localConsoleButtonText = - LCManager.shared.isVisible ? "Close Local Console" : "Enable Local Console" - - } label: { - Label { - Text(localConsoleButtonText) - .foregroundStyle(.foreground) - } icon: { - Image(systemSymbol: .macwindow) - .symbolRenderingMode(.multicolor) - } - } - } - // MARK: - DEBUG AND PULSE Section { @@ -152,30 +130,6 @@ struct SettingsDevOptionsView: View { } } - Toggle(isOn: $realmExperimentalViewEnabled) { - Label { - Text("Realm Experimental View") - } icon: { - Image(systemSymbol: .circleAndLineHorizontalFill) - .foregroundStyle(.pink) - .symbolRenderingMode(.hierarchical) - } - } - .tint(accentColor) - - // NavigationLink { - // OfflineDownloaderView() - // } label: { - // Label { - // Text("Offline Downloader") - // } icon: { - // Image(systemSymbol: .squareAndArrowDownFill) - // .foregroundStyle(.cyan) - // .brightness(-0.2) - // .symbolRenderingMode(.hierarchical) - // } - // } - NavigationLink { ColorTesterView() } label: { diff --git a/Lunar/Settings Views/SettingsLayoutView.swift b/Lunar/Settings Views/SettingsLayoutView.swift deleted file mode 100644 index 7977c500..00000000 --- a/Lunar/Settings Views/SettingsLayoutView.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// SettingsLayoutView.swift -// Lunar -// -// Created by Mani on 27/07/2023. -// - -import Defaults -import SwiftUI - -enum PostsViewStyle: String, Defaults.Serializable { - case large - case compact -} - -struct SettingsLayoutView: View { - @Default(.commentMetadataPosition) var commentMetadataPosition - @Default(.detailedCommunityLabels) var detailedCommunityLabels - @Default(.postsViewStyle) var postsViewStyle - @Default(.autoCollapseBots) var autoCollapseBots - @Default(.accentColor) var accentColor - - var body: some View { - List { - // MARK: - Posts Section - - Section { - postsPageStylePicker - - } header: { - Text("Posts") - } - - // MARK: - Comments Section - - Section { - commentMetadataLayoutPicker - autoCollapseBotsToggle - - } header: { - Text("Comments") - } footer: { - Text("Changes the position of upvotes, downvotes, username, and time posted to either above or below the comment text.") - } - - // MARK: - Labels Section - - Section { - detailedCommunityLabelsToggle - } header: { - Text("Labels") - } - } - .navigationTitle("Layout") - } - - var postsPageStylePicker: some View { - Picker("Posts Style", selection: $postsViewStyle) { - Text("Large").tag(PostsViewStyle.large) - Text("Compact").tag(PostsViewStyle.compact) - } - .pickerStyle(.menu) - } - - var commentMetadataLayoutPicker: some View { - Picker("Comment Metadata Position", selection: $commentMetadataPosition) { - Text("Bottom").tag("Bottom") - Text("Top").tag("Top") - Text("None").tag("None") - } - .pickerStyle(.menu) - } - - var autoCollapseBotsToggle: some View { - Toggle(isOn: $autoCollapseBots) { - Text("Auto-collapse Bots") - } - .tint(accentColor) - } - - var detailedCommunityLabelsToggle: some View { - Toggle(isOn: $detailedCommunityLabels) { - Text("Detailed Community Labels") - } - .tint(accentColor) - } -} - -#Preview { - SettingsLayoutView() -} diff --git a/Lunar/Settings Views/SettingsQuicklinksView.swift b/Lunar/Settings Views/SettingsQuicklinksView.swift index 21dc6ce9..96913df4 100644 --- a/Lunar/Settings Views/SettingsQuicklinksView.swift +++ b/Lunar/Settings Views/SettingsQuicklinksView.swift @@ -35,16 +35,6 @@ struct SettingsQuicklinksView: View { var iconList: [String] = CircleFillIcons().iconsList() var body: some View { - /// **Future implementation** - // DroppableList("Users 1", users: $users1) { dropped, index in - // users1.insert(dropped, at: index) - // users2.removeAll { $0 == dropped } - // } - // DroppableList("Users 2", users: $users2) { dropped, index in - // users2.insert(dropped, at: index) - // users1.removeAll { $0 == dropped } - // } - List { Section { Toggle(isOn: $enableQuicklinks) { @@ -178,19 +168,6 @@ struct SettingsQuicklinksView: View { SortPicker(sortType: $quicklinkSort) .labelStyle(TitleOnlyLabelStyle()) -// Picker("Post Sort", selection: $quicklinkSort) { -// Text("Active").tag("Active") -// Text("Hot").tag("Hot") -// Text("New").tag("New") -// Text("Top Day").tag("TopDay") -// Text("Top Week").tag("TopWeek") -// Text("Top Month").tag("TopMonth") -// Text("Top Year").tag("TopYear") -// Text("Top All").tag("TopAll") -// Text("Most Comments").tag("MostComments") -// Text("New Comments").tag("NewComments") -// } -// .pickerStyle(.menu) } header: { Text("Type") } diff --git a/Lunar/Settings Views/SettingsSplashScreenView.swift b/Lunar/Settings Views/SettingsSplashScreenView.swift deleted file mode 100644 index a59f7ef2..00000000 --- a/Lunar/Settings Views/SettingsSplashScreenView.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SettingsSplashScreenView.swift -// Lunar -// -// Created by Mani on 22/08/2023. -// - -import Defaults -import SwiftUI -import WhatsNewKit - -struct SettingsSplashScreenView: View { - @Default(.showLaunchSplashScreen) var showLaunchSplashScreen - @Default(.clearWhatsNewDefaults) var clearWhatsNewDefaults - @Default(.clearInitialWhatsNewDefault) var clearInitialWhatsNewDefault - @Default(.accentColor) var accentColor - - @State var alertPresented: Bool = false - - let notificationHaptics = UINotificationFeedbackGenerator() - - var body: some View { - List { - Section { - Toggle(isOn: $showLaunchSplashScreen) { - Text("Launch Splash Screen") - } - .tint(accentColor) - - } footer: { - Text("Show the Lunar logo splash screen for a couple of seconds every time the app is launched.") - } - Section { - Button { - clearWhatsNewDefaults.toggle() - notificationHaptics.notificationOccurred(.success) - alertPresented = true - } label: { - Text("Clear All WhatsNewKit List") - } - Button { - clearInitialWhatsNewDefault.toggle() - notificationHaptics.notificationOccurred(.success) - alertPresented = true - } label: { - Text("Clear WhatsNewKit Initial Launch") - } - } - } - .alert(isPresented: $alertPresented) { - Alert(title: Text("Cleared")) - } - .navigationTitle("Splash Screen") - } -} - -#Preview { - SettingsSplashScreenView() -} diff --git a/Lunar/Settings Views/SettingsThemeView.swift b/Lunar/Settings Views/SettingsThemeView.swift index 9a953e8c..033cd591 100644 --- a/Lunar/Settings Views/SettingsThemeView.swift +++ b/Lunar/Settings Views/SettingsThemeView.swift @@ -1,101 +1,90 @@ +//// +//// SettingsThemeView.swift +//// Lunar +//// +//// Created by Mani on 27/07/2023. +//// // -// SettingsThemeView.swift -// Lunar +// import Defaults +// import SFSafeSymbols +// import SwiftUI // -// Created by Mani on 27/07/2023. +// struct SettingsThemeView: View { +// @AppStorage("appAppearance") var appAppearance: AppearanceOptions = .system // - -import Defaults -import SFSafeSymbols -import Shiny -import SwiftUI - -struct SettingsThemeView: View { - @AppStorage("appAppearance") var appAppearance: AppearanceOptions = .system - - @Default(.iridescenceEnabled) var iridescenceEnabled - @Default(.accentColor) var accentColor - @Default(.accentColorString) var accentColorString - - var accentColorsList: [String] = [ - "blue", - "indigo", - "purple", - "pink", - "red", - "orange", - "yellow", - "brown", - "green", - "mint", - "cyan", - "gray", - ] - - var body: some View { - List { - Section { - appearancePicker - accentColorPicker - } - iridescenceSection - } - .navigationTitle("Theme") - } - - var appearancePicker: some View { - Picker("Appearance", selection: $appAppearance) { - ForEach(AppearanceOptions.allCases, id: \.self) { option in - Text(option.rawValue.capitalized) - } - } - .pickerStyle(.menu) - .onChange(of: appAppearance) { _ in - AppearanceController.shared.setAppearance() - } - } - - var accentColorPicker: some View { - HStack { - Text("Accent Color") - Spacer() - Menu { - Button { - accentColor = ColorConverter().convertStringToColor("blue") - accentColorString = "Default" - } label: { - Text("Default") - } - ForEach(accentColorsList, id: \.self) { color in - HStack { - Button { - accentColor = ColorConverter().convertStringToColor(color) - accentColorString = color.capitalized - } label: { - Text(color.capitalized) - } - } - } - - } label: { - Text(accentColorString) - .foregroundStyle(accentColor) - .tint(accentColor) - } - } - } - - var iridescenceSection: some View { - Section { - Toggle(isOn: $iridescenceEnabled) { - Text("Iridescent Effects") - } - .tint(accentColorString == "Default" ? .indigo : accentColor) - } - .modifier(ConditionalListRowBackgroundModifier(background: iridescenceEnabled ? .iridescent : .defaultBackground)) - } -} - -#Preview { - SettingsThemeView() -} +// @Default(.accentColor) var accentColor +// @Default(.accentColorString) var accentColorString +// +// var accentColorsList: [String] = [ +// "blue", +// "indigo", +// "purple", +// "pink", +// "red", +// "orange", +// "yellow", +// "brown", +// "green", +// "mint", +// "cyan", +// "gray", +// ] +// +// var body: some View { +// List { +// Section { +// appearancePicker +// accentColorPicker +// } +// iridescenceSection +// } +// .navigationTitle("Theme") +// } +// +// var appearancePicker: some View { +// Picker("Appearance", selection: $appAppearance) { +// ForEach(AppearanceOptions.allCases, id: \.self) { option in +// Text(option.rawValue.capitalized) +// } +// } +// .pickerStyle(.menu) +// .onChange(of: appAppearance) { _ in +// AppearanceController.shared.setAppearance() +// } +// } +// +// var accentColorPicker: some View { +// HStack { +// Text("Accent Color") +// Spacer() +// Menu { +// Button { +// accentColor = ColorConverter().convertStringToColor("blue") +// accentColorString = "Default" +// } label: { +// Text("Default") +// } +// ForEach(accentColorsList, id: \.self) { color in +// HStack { +// Button { +// accentColor = ColorConverter().convertStringToColor(color) +// accentColorString = color.capitalized +// } label: { +// Text(color.capitalized) +// } +// } +// } +// +// } label: { +// Text(accentColorString) +// .foregroundStyle(accentColor) +// .tint(accentColor) +// } +// } +// } +// +// } +// +// #Preview { +// SettingsThemeView() +// } diff --git a/Lunar/Settings Views/SettingsView.swift b/Lunar/Settings Views/SettingsView.swift index 482206e3..511a8f91 100644 --- a/Lunar/Settings Views/SettingsView.swift +++ b/Lunar/Settings Views/SettingsView.swift @@ -92,69 +92,70 @@ struct SettingsView: View { } label: { Text("Manage Instances") } - /// KbinSelectorView() - Moved to instance selector view } } - var notificationsNavLink: some View { - NavigationLink { - PlaceholderView() - } label: { - Label { - Text("Notifications") - } icon: { - AllSymbols().notificationsSettings - } - } - } - - var gesturesNavLink: some View { - NavigationLink { - PlaceholderView() - } label: { - Label { - Text("Gestures") - } icon: { - AllSymbols().gesturesSettings - } - } - } - - var soundAndHapticsNavLink: some View { - NavigationLink { - PlaceholderView() - } label: { - Label { - Text("Sounds and Haptics") - } icon: { - AllSymbols().soundAndHapticsSettings - } - } - } - - var composerNavLink: some View { - NavigationLink { - PlaceholderView() - } label: { - Label { - Text("Composer") - } icon: { - AllSymbols().composerSettings - } - } - } - - var searchNavLink: some View { - NavigationLink { - PlaceholderView() - } label: { - Label { - Text("Search") - } icon: { - AllSymbols().searchSettings - } - } - } + /* + var notificationsNavLink: some View { + NavigationLink { + PlaceholderView() + } label: { + Label { + Text("Notifications") + } icon: { + AllSymbols().notificationsSettings + } + } + } + + var gesturesNavLink: some View { + NavigationLink { + PlaceholderView() + } label: { + Label { + Text("Gestures") + } icon: { + AllSymbols().gesturesSettings + } + } + } + + var soundAndHapticsNavLink: some View { + NavigationLink { + PlaceholderView() + } label: { + Label { + Text("Sounds and Haptics") + } icon: { + AllSymbols().soundAndHapticsSettings + } + } + } + + var composerNavLink: some View { + NavigationLink { + PlaceholderView() + } label: { + Label { + Text("Composer") + } icon: { + AllSymbols().composerSettings + } + } + } + + var searchNavLink: some View { + NavigationLink { + PlaceholderView() + } label: { + Label { + Text("Search") + } icon: { + AllSymbols().searchSettings + } + } + } + */ var quicklinksNavLink: some View { NavigationLink { @@ -176,7 +177,7 @@ struct SettingsView: View { Text("App Icons") } icon: { // Till 03/01/2024 - if Date().timeIntervalSince1970 < 1704301200 { + if Date().timeIntervalSince1970 < 1_704_301_200 { AllSymbols().christmasAppIconSettings } else { AllSymbols().appIconSettings @@ -197,29 +198,19 @@ struct SettingsView: View { } } - var themeNavLink: some View { - NavigationLink { - SettingsThemeView() - } label: { - Label { - Text("Theme") - } icon: { - AllSymbols().themeSettings - } - } - } - - var layoutNavLink: some View { - NavigationLink { - SettingsLayoutView() - } label: { - Label { - Text("Layout") - } icon: { - AllSymbols().layoutSettings + /* + var themeNavLink: some View { + NavigationLink { + SettingsThemeView() + } label: { + Label { + Text("Theme") + } icon: { + AllSymbols().themeSettings + } } } - } + */ var privacyPolicyButton: some View { Button { @@ -399,74 +390,9 @@ struct SettingsView: View { } } -// @ViewBuilder -// var appCreditsAndInfo: some View { -// Group { -// HStack { -// Spacer() -// VStack(alignment: .center, spacing: 2) { -// Text("Lunar v\(appVersion)") -// Text(LocalizedStringKey("~ by [mani](https://github.com/mani-sh-reddy) ~")) -// } -// Spacer() -// } -// HStack { -// Spacer() -// emailButtonSmall -// .padding(.top, 20) -// Spacer() -// } -// HStack { -// Spacer() -// matrixButtonSmall -// .padding(.bottom, 10) -// Spacer() -// } -// } -// -// .font(.caption) -// .foregroundStyle(.secondary) -// .listRowSeparator(.hidden) -// .listRowBackground(Color.clear) -// } -// -// var matrixButtonSmall: some View { -// Button { -// notificationHaptics.notificationOccurred(.success) -// withAnimation { -// matrixCopiedToClipboard = true -// } -// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { -// withAnimation { -// matrixCopiedToClipboard = false -// } -// } -// -// UIPasteboard.general -// .setValue( -// "@mani.sh:matrix.org", -// forPasteboardType: UTType.plainText.identifier -// ) -// } label: { -// Text(matrixCopiedToClipboard ? "Copied to clipboard" : "@mani.sh:matrix.org") -// } -// .foregroundStyle(accentColorString == "Default" ? .blue : accentColor) -// } -// -// var emailButtonSmall: some View { -// Button { -// Mailto().mailto("lunarforlemmy@outlook.com") -// } label: { -// Text("lunarforlemmy@outlook.com") -// } -// .foregroundStyle(accentColorString == "Default" ? .blue : accentColor) -// } -// } - #Preview { List { SettingsView().appCreditsAndInfo SettingsView().appIconNavLink } -// SettingsView() } diff --git a/Lunar/Tools/ConditionalModifiers.swift b/Lunar/Tools/ConditionalModifiers.swift index e35d2e0b..d968b931 100644 --- a/Lunar/Tools/ConditionalModifiers.swift +++ b/Lunar/Tools/ConditionalModifiers.swift @@ -48,7 +48,7 @@ struct ConditionalListRowBackgroundModifier: ViewModifier { content.listRowBackground( ZStack { Color.gray.opacity(0.1) - Rectangle().shiny(.iridescent) + Rectangle() } )) } else if background == .defaultBackground { diff --git a/Lunar/Tools/RealmThawFunctions.swift b/Lunar/Tools/RealmThawFunctions.swift index 4911def0..e1d6b437 100644 --- a/Lunar/Tools/RealmThawFunctions.swift +++ b/Lunar/Tools/RealmThawFunctions.swift @@ -22,6 +22,19 @@ class RealmThawFunctions { } } + func deleteAction(post: RealmPost) { + let thawedPost = post.thaw() + if thawedPost?.isInvalidated == false { + let thawedRealm = thawedPost!.realm! + try! thawedRealm.write { + if let thawedPost { + thawedRealm.delete(thawedPost) + } + } + } + hapticsSoft.impactOccurred(intensity: 0.5) + } + func hideAction(post: RealmPost) { let realm = try! Realm() try! realm.write { diff --git a/Lunar/Tools/Utilities/URLBuilder.swift b/Lunar/Tools/Utilities/URLBuilder.swift deleted file mode 100644 index dfa10011..00000000 --- a/Lunar/Tools/Utilities/URLBuilder.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// URLBuilder.swift -// Lunar -// -// Created by Mani on 23/07/2023. -// - -import Defaults -import Foundation -import SwiftUI - -/// **Base URL** -/// https://lemmy.world/api/v3/ -/// **Communities List** -/// community/list?type_=Local&sort=Active&page=1&limit=10 -/// **Community Specific Posts** -/// post/list?type_=Local&sort=Active&page=1&limit=10&community_id=145566 -/// **Non Specific Posts** -/// post/list?type_=Local&sort=Active&page=1&limit=10 -/// **Comments List** -/// comment/list?type_=Local&sort=Top&max_depth=2&page=1&limit=10&post_id=2021423 - -class URLBuilder { - @Default(.selectedInstance) var selectedInstance - - private let endpointPath: String - private let sortParameter: String? - private let typeParameter: String? - private let currentPage: Int? - private let limitParameter: Int? - private let savedOnly: Bool? - private let communityID: Int? - private let personID: Int? - private let postID: Int? - private let maxDepth: Int? - private let jwt: String? - private let searchQuery: String? - private let listingType: String? - private let voteType: Int? - private let instance: String? - private let urlString: String? - - init( - endpointPath: String, - sortParameter: String? = "Hot", - typeParameter: String? = "", - currentPage: Int? = 1, - limitParameter: Int? = nil, - savedOnly: Bool? = nil, - communityID: Int? = nil, - personID: Int? = nil, - postID: Int? = nil, - maxDepth: Int? = nil, - jwt: String? = nil, - searchQuery: String? = nil, - listingType: String? = nil, - voteType: Int? = nil, - instance: String? = nil, - urlString: String? = nil - ) { - self.endpointPath = endpointPath - self.sortParameter = sortParameter - self.typeParameter = typeParameter - self.currentPage = currentPage - self.limitParameter = limitParameter - self.savedOnly = savedOnly - self.communityID = communityID - self.personID = personID - self.postID = postID - self.maxDepth = maxDepth - self.jwt = jwt - self.searchQuery = searchQuery - self.listingType = listingType - self.voteType = voteType - self.instance = instance - self.urlString = urlString - } - - func buildURL() -> URLComponents { - var endpoint = URLComponents() - var queryParams: [String: String?] = [:] - - if let sortParameter { queryParams["sort"] = String(sortParameter) } - if let typeParameter { queryParams["type_"] = String(typeParameter) } - if let currentPage { queryParams["page"] = String(currentPage) } - if let limitParameter { queryParams["limit"] = String(limitParameter) } - if let savedOnly { queryParams["saved_only"] = String(savedOnly) } - if let communityID, communityID != 0 { queryParams["community_id"] = String(communityID) } - if let personID, personID != 0 { queryParams["person_id"] = String(personID) } - if let postID { queryParams["post_id"] = String(postID) } - if let maxDepth { queryParams["max_depth"] = String(maxDepth) } - if let jwt { queryParams["auth"] = jwt } - if let searchQuery { queryParams["q"] = String(searchQuery) } - if let listingType { queryParams["listing_type"] = String(listingType) } - if let voteType { queryParams["score"] = String(voteType) } - if let urlString { queryParams["url"] = String(urlString) } - - endpoint.scheme = "https" - - if let instance { - endpoint.host = instance - } else { - endpoint.host = selectedInstance - } - - endpoint.path = endpointPath - endpoint.setQueryItems(with: queryParams) - - return endpoint - } -} diff --git a/Lunar/Tools/Utilities/URLParser.swift b/Lunar/Tools/Utilities/URLParser.swift index 8f2b8f73..b617bdf1 100644 --- a/Lunar/Tools/Utilities/URLParser.swift +++ b/Lunar/Tools/Utilities/URLParser.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI enum URLParser { - /// "https://lemmy.world/c/mani" ==> _lemmy.world_ + /// "https://lemmy.world/u/mani" ==> _lemmy.world_ static func extractDomain(from url: String) -> String { guard let urlComponents = URLComponents(string: url), let host = urlComponents.host @@ -37,7 +37,7 @@ enum URLParser { return path } - /// "https://lemmy.world/c/mani" ==> _mani_ + /// "https://lemmy.world/u/mani" ==> _mani_ static func extractUsername(from url: String) -> String { let path = extractPath(from: url) if let range = path.range(of: "/u/", options: .regularExpression) { @@ -54,4 +54,14 @@ enum URLParser { return components.penultimate() ?? "" } + + /// "https://lemmy.world/u/mani" ==> _mani@lemmy.world_ + static func buildFullUsername(from url: String) -> String { + "\(extractUsername(from: url))@\(extractDomain(from: url))" + } + + /// "https://lemmy.world/c/asklemmy" ==> _asklemmy@lemmy.world_ + static func buildFullCommunity(from communityActorID: String) -> String { + "\(extractUsername(from: communityActorID))@\(extractDomain(from: communityActorID))" + } }