diff --git a/Podfile b/Podfile index b3643ee920..33024650e3 100644 --- a/Podfile +++ b/Podfile @@ -26,7 +26,6 @@ def all_pods pod 'SVProgressHUD', '2.2.5' # TSMessages is no longer being maintained/updated, remove or migrate to RMessage/SwiftMessages pod 'TSMessages', :git => 'https://github.com/KrauseFx/TSMessages.git' - pod 'YandexMobileMetrica/Dynamic', '3.8.2' pod 'SnapKit', '4.2.0' @@ -36,7 +35,9 @@ def all_pods pod 'Firebase/Analytics' , '6.10.0' pod 'Firebase/RemoteConfig', '6.10.0' + pod 'YandexMobileMetrica/Dynamic', '3.8.2' pod 'Amplitude-iOS', '4.8.2' + pod 'Branch', '0.25.11' pod 'BEMCheckBox', '1.4.1' @@ -64,7 +65,7 @@ def all_pods pod 'Nuke', '7.6.3' pod 'STRegex', '2.1.0' pod 'Tabman', '2.4.3' - pod 'Branch', '0.25.11' + pod 'SwiftDate', '6.1.0' end def testing_pods diff --git a/Podfile.lock b/Podfile.lock index 93e84e27ba..395012d7d0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -169,6 +169,7 @@ PODS: - SVGKit (2.1.0): - CocoaLumberjack (~> 3.0) - SVProgressHUD (2.2.5) + - SwiftDate (6.1.0) - SwiftLint (0.35.0) - SwiftyGif (5.1.1) - SwiftyJSON (5.0.0) @@ -227,6 +228,7 @@ DEPENDENCIES: - STRegex (= 2.1.0) - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `2.x`) - SVProgressHUD (= 2.2.5) + - SwiftDate (= 6.1.0) - SwiftLint (= 0.35.0) - SwiftyJSON (= 5.0.0) - Tabman (= 2.4.3) @@ -291,6 +293,7 @@ SPEC REPOS: - SnapKit - STRegex - SVProgressHUD + - SwiftDate - SwiftLint - SwiftyGif - SwiftyJSON @@ -378,6 +381,7 @@ SPEC CHECKSUMS: STRegex: dfa420d93d8c1402956233b3879ec1fc14b45fbe SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 + SwiftDate: fa2bb3962056bb44047b4b85a30044e5eae30b03 SwiftLint: 5553187048b900c91aa03552807681bb6b027846 SwiftyGif: f7702483db93586a41f04f4927cd682852a2fa10 SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 @@ -389,6 +393,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 62a10b6571fbcda0657f455fedce7fedf55b4cd0 YandexMobileMetrica: edb00e8af2903290e142ba4c488adf8d394e828a -PODFILE CHECKSUM: dc63d7ff94795a04c64f1ed6339d1e679ac8adcd +PODFILE CHECKSUM: a301f6a0304d923e0bdf9c0ddf1212d1abee6fdb COCOAPODS: 1.8.4 diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index d790f78222..b471e8cc51 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -345,7 +345,6 @@ 08E7CA1020DAF6E0004F8563 /* AnalyticsUserProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E7CA0F20DAF6E0004F8563 /* AnalyticsUserProperties.swift */; }; 08EB85DF1D0F192900E4F345 /* LoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EB85DD1D0F192900E4F345 /* LoadMoreTableViewCell.swift */; }; 08EB85E11D0F192A00E4F345 /* LoadMoreTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08EB85DE1D0F192900E4F345 /* LoadMoreTableViewCell.xib */; }; - 08EB85F01D101D7800E4F345 /* WriteCommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EB85EE1D101D7800E4F345 /* WriteCommentViewController.swift */; }; 08EB85F81D10454D00E4F345 /* DiscussionsStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08EB85F71D10454D00E4F345 /* DiscussionsStoryboard.storyboard */; }; 08EB85FB1D10863F00E4F345 /* DiscussionAlertConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EB85FA1D10863F00E4F345 /* DiscussionAlertConstructor.swift */; }; 08EDD62A1F7C6785005203E4 /* StepikButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EDD6291F7C6785005203E4 /* StepikButton.swift */; }; @@ -380,12 +379,15 @@ 08FEFC211F127470005CA0FB /* AutocompleteWords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FEFC201F127470005CA0FB /* AutocompleteWords.swift */; }; 0B55A18E03044AF8E4C4E730 /* NewSortingQuizPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBD3EFC6CF6DE0ADC84018 /* NewSortingQuizPresenter.swift */; }; 0BC1205C2851D3AFE8026893 /* NewMatchingQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547578D4D5EFD63704E87CAB /* NewMatchingQuizDataFlow.swift */; }; + 0C97AFADC2099B3785D9B910 /* WriteCommentInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACBF39DAC4A4A599F8949F2 /* WriteCommentInteractor.swift */; }; 0D389430943C20609C095B3D /* NewChoiceQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508C25A19DBEE7176400D767 /* NewChoiceQuizView.swift */; }; + 0E3927B3DADE084DA71C868B /* WriteCommentAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F53BF6D2A309CFC55A72D5 /* WriteCommentAssembly.swift */; }; 0E600BAD134430EDC4B7B683 /* ProfileEditAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4D51541BF7AE1FCD2865C0 /* ProfileEditAssembly.swift */; }; 0E8FC0797A66F24878602C13 /* NewCodeQuizFullscreenOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCCE000FB8689122DC202C3E /* NewCodeQuizFullscreenOutputProtocol.swift */; }; 0FB3527D1A23BBFBC2DEC937 /* SettingsStepFontSizeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA402843D014B6EBD94BADDA /* SettingsStepFontSizeDataFlow.swift */; }; 0FC4DF01DB2BF054237EBD04 /* NewChoiceQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077CA2A02D61A314B281C7C3 /* NewChoiceQuizInteractor.swift */; }; 10B5CCF7E39ED3E7BB8382F6 /* SettingsStepFontSizeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35918ED4BFF0FC4C8692E8A6 /* SettingsStepFontSizeInteractor.swift */; }; + 10F32F9073D41A146E8A3994 /* WriteCommentOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 995EDC0C4A4B7085EE7589C2 /* WriteCommentOutputProtocol.swift */; }; 18DBB0FACACD30EC7F311E3C /* NewCodeQuizFullscreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081C378AAE221323BF85346D /* NewCodeQuizFullscreenView.swift */; }; 18E3319B16168E42505378AB /* SettingsStepFontSizePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3934F4CAA53B3137AE330BC4 /* SettingsStepFontSizePresenter.swift */; }; 19CEA9C063AE5CD5A682CFA9 /* ProfileEditInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5BA4688D7B46C416231BD9 /* ProfileEditInteractor.swift */; }; @@ -570,6 +572,7 @@ 2C98B6B61FDFD74C005AB72C /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C98B6B51FDFD74C005AB72C /* OnboardingViewController.swift */; }; 2C9A8D2E22D348A5009434DB /* String+HTMLEscape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9A8D2D22D348A5009434DB /* String+HTMLEscape.swift */; }; 2C9BD78E1FC43C6B00F89CBE /* NotificationsBadgesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9BD78D1FC43C6B00F89CBE /* NotificationsBadgesManager.swift */; }; + 2C9C0E0F235F561A00EC31F3 /* WriteCommentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9C0E0E235F561A00EC31F3 /* WriteCommentViewModel.swift */; }; 2C9C9772230D73F900E44F68 /* NewSortingQuizElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9C9771230D73F800E44F68 /* NewSortingQuizElementView.swift */; }; 2C9E3F3C1F7A80A300DDF1AA /* Notification+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E3F3B1F7A80A300DDF1AA /* Notification+CoreDataProperties.swift */; }; 2C9E3F3E1F7A930100DDF1AA /* NotificationsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E3F3D1F7A930100DDF1AA /* NotificationsAPI.swift */; }; @@ -688,6 +691,7 @@ 2CFC5ABC228ADFC400B5248A /* StepsPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFC5ABB228ADFC400B5248A /* StepsPersistenceService.swift */; }; 2CFC5ABE228AF0B400B5248A /* NewLessonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFC5ABD228AF0B400B5248A /* NewLessonViewModel.swift */; }; 2CFDB1241F559F9A00B8035C /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFDB1231F559F9A00B8035C /* AvatarImageView.swift */; }; + 2D6AB32933F1C93FCF849056 /* WriteCommentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77A40A485EDD8A3E4AA0157 /* WriteCommentProvider.swift */; }; 3203AD6A1594995EDE114EA0 /* Pods_StepicTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */; }; 3317FC9AECA8FF54902BF61D /* NewChoiceQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E041388C85D190467E74A5 /* NewChoiceQuizViewController.swift */; }; 3382D26FC5E2AB43D5CD0B71 /* NewFreeAnswerQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BB01DDF94CCF5E5EBBB43 /* NewFreeAnswerQuizViewController.swift */; }; @@ -701,6 +705,7 @@ 3E2FBE43761DC0D89BD35ED1 /* NewSortingQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6520F5CFAE56B9CFBA963C1D /* NewSortingQuizInteractor.swift */; }; 4300EB39EFE2C4ABA5FA1CC5 /* SettingsStepFontSizeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084CF16CCDB9103E521B889D /* SettingsStepFontSizeProvider.swift */; }; 47AA3FAF72E7AD067ABF2804 /* NewFreeAnswerQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF50F304BF0016F5B477EEC8 /* NewFreeAnswerQuizDataFlow.swift */; }; + 49D8E231DE76283BD37FFD3B /* WriteCommentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44BBB1022CD4FB280EB7DFB /* WriteCommentPresenter.swift */; }; 4AC49732BD397C516D132297 /* NewLessonPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC0A2EAEB9E30B54695D44A /* NewLessonPresenter.swift */; }; 4E3A4636F1A4E8E38E0AB66B /* NewChoiceQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAECD143E5E804E55A9DE096 /* NewChoiceQuizDataFlow.swift */; }; 580F18AF73EFFCBF1EDED227 /* NewStringQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D70A098825966D4584016F /* NewStringQuizViewController.swift */; }; @@ -970,9 +975,11 @@ 682816CD6963D970C0C4374E /* SettingsStepFontSizeAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F67C33C94A34359300BFA21 /* SettingsStepFontSizeAssembly.swift */; }; 68CF5EDC9EC0D659F78BDE54 /* NewCodeQuizFullscreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3417B44078A2AB57A0E63F /* NewCodeQuizFullscreenPresenter.swift */; }; 6E7B886BB0F8832440F729A3 /* UnsupportedQuizPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8E4DEE020E670D88BD7FEA /* UnsupportedQuizPresenter.swift */; }; + 7060C8E8055EC26669A7051B /* WriteCommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCE255730E82181FAAF0AAA /* WriteCommentViewController.swift */; }; 70B5E9BE261BB46951DCA892 /* NewFreeAnswerQuizAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6741032B8AF7696CAC7126 /* NewFreeAnswerQuizAssembly.swift */; }; 725D1718466B8BC980C95AFB /* WriteCourseReviewAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED1D58A415B3B4B592268DA /* WriteCourseReviewAssembly.swift */; }; 7AC15207BB66E845FDBEE234 /* NewCodeQuizFullscreenInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3025F71BBEA83B2F2D4EE286 /* NewCodeQuizFullscreenInteractor.swift */; }; + 7D334A7F2599276A266E17AE /* WriteCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C65429C78822944952072FB /* WriteCommentView.swift */; }; 7FBB25F3FD251734F7E049DD /* NewStepDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49034C40838BCF58896D0118 /* NewStepDataFlow.swift */; }; 857E1FD24A70F87BCA6EBABA /* NewStringQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2111290D2E9D752A22AFC024 /* NewStringQuizInteractor.swift */; }; 85CD3504769C1B6C8C21661A /* NewMatchingQuizAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC73A992112910D33A82457 /* NewMatchingQuizAssembly.swift */; }; @@ -989,6 +996,7 @@ 8DF8D727FFAF37772B6AE2B9 /* WriteCourseReviewInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D74D3F78768D42D919852E /* WriteCourseReviewInteractor.swift */; }; 92E876582D263061F44316EB /* NewFreeAnswerQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F7667856CC2C2A2AC43631 /* NewFreeAnswerQuizView.swift */; }; 97B20DBCAE18BA196B55CA91 /* WriteCourseReviewDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C485A2D511BCFEA6E85B22F /* WriteCourseReviewDataFlow.swift */; }; + A0A7AC991F92BEC73820B298 /* WriteCommentDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DBF5A43025E726B6D61E1E /* WriteCommentDataFlow.swift */; }; B05BCD4B9FBDCF94AF7DB650 /* NewDiscussionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B671AACF375B3ED49120185 /* NewDiscussionsPresenter.swift */; }; B0D23296AB3FAC5BC6F45029 /* NewLessonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC193B8E01FF4A61441ABF13 /* NewLessonViewController.swift */; }; B8042A238380ACC1F32082E1 /* NewStepAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1AB142BF62815DF8939656 /* NewStepAssembly.swift */; }; @@ -1446,7 +1454,6 @@ 08E8F9791F34E64E008CF4A1 /* SearchSuggestionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SearchSuggestionTableViewCell.xib; sourceTree = ""; }; 08EB85DD1D0F192900E4F345 /* LoadMoreTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreTableViewCell.swift; sourceTree = ""; }; 08EB85DE1D0F192900E4F345 /* LoadMoreTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LoadMoreTableViewCell.xib; sourceTree = ""; }; - 08EB85EE1D101D7800E4F345 /* WriteCommentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WriteCommentViewController.swift; sourceTree = ""; }; 08EB85F71D10454D00E4F345 /* DiscussionsStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DiscussionsStoryboard.storyboard; sourceTree = ""; }; 08EB85FA1D10863F00E4F345 /* DiscussionAlertConstructor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DiscussionAlertConstructor.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 08EDD6291F7C6785005203E4 /* StepikButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepikButton.swift; sourceTree = ""; }; @@ -1481,6 +1488,7 @@ 08FEFC1B1F117257005CA0FB /* CodeSuggestionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CodeSuggestionTableViewCell.xib; sourceTree = ""; }; 08FEFC201F127470005CA0FB /* AutocompleteWords.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteWords.swift; sourceTree = ""; }; 132A23540B83F9E4658F740C /* NewLessonAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonAssembly.swift; sourceTree = ""; }; + 1C65429C78822944952072FB /* WriteCommentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentView.swift; sourceTree = ""; }; 20DD1856CD25464BAC75EE10 /* NewDiscussionsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewDiscussionsInteractor.swift; sourceTree = ""; }; 2111290D2E9D752A22AFC024 /* NewStringQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStringQuizInteractor.swift; sourceTree = ""; }; 21A89AE8D9541C3D75F0D343 /* NewStepInputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepInputProtocol.swift; sourceTree = ""; }; @@ -1674,6 +1682,7 @@ 2C98B6B51FDFD74C005AB72C /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 2C9A8D2D22D348A5009434DB /* String+HTMLEscape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTMLEscape.swift"; sourceTree = ""; }; 2C9BD78D1FC43C6B00F89CBE /* NotificationsBadgesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsBadgesManager.swift; sourceTree = ""; }; + 2C9C0E0E235F561A00EC31F3 /* WriteCommentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCommentViewModel.swift; sourceTree = ""; }; 2C9C9771230D73F800E44F68 /* NewSortingQuizElementView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewSortingQuizElementView.swift; sourceTree = ""; }; 2C9E3F351F7A79E600DDF1AA /* Model_notifications.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_notifications.xcdatamodel; sourceTree = ""; }; 2C9E3F3B1F7A80A300DDF1AA /* Notification+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+CoreDataProperties.swift"; sourceTree = ""; }; @@ -1803,6 +1812,7 @@ 35F47B9DDB10472D2B2542B4 /* NewCodeQuizFullscreenViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenViewController.swift; sourceTree = ""; }; 362684BA69196E87B5919671 /* NewStepProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepProvider.swift; sourceTree = ""; }; 36E041388C85D190467E74A5 /* NewChoiceQuizViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewChoiceQuizViewController.swift; sourceTree = ""; }; + 36F53BF6D2A309CFC55A72D5 /* WriteCommentAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentAssembly.swift; sourceTree = ""; }; 37D0922F478391C8B44EA5D0 /* NewFreeAnswerQuizPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewFreeAnswerQuizPresenter.swift; sourceTree = ""; }; 3934F4CAA53B3137AE330BC4 /* SettingsStepFontSizePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsStepFontSizePresenter.swift; sourceTree = ""; }; 3AD36917F532D0F83798B2F2 /* NewChoiceQuizAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewChoiceQuizAssembly.swift; sourceTree = ""; }; @@ -1816,6 +1826,7 @@ 49B8797DC84D64C5BAA84E76 /* Pods-Stepic.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.debug.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.debug.xcconfig"; sourceTree = ""; }; 4A4D51541BF7AE1FCD2865C0 /* ProfileEditAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileEditAssembly.swift; sourceTree = ""; }; 4C9CD16032427B60FC4C99EF /* ProfileEditDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileEditDataFlow.swift; sourceTree = ""; }; + 4CCE255730E82181FAAF0AAA /* WriteCommentViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentViewController.swift; sourceTree = ""; }; 4F646AB1EA500AD2EB797350 /* NewDiscussionsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewDiscussionsProvider.swift; sourceTree = ""; }; 4FBB4E2D74ED45195B3EF372 /* NewCodeQuizFullscreenAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenAssembly.swift; sourceTree = ""; }; 508C25A19DBEE7176400D767 /* NewChoiceQuizView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewChoiceQuizView.swift; sourceTree = ""; }; @@ -2100,10 +2111,12 @@ 90E18232B2A8EC99F6ACBEF3 /* WriteCourseReviewOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCourseReviewOutputProtocol.swift; sourceTree = ""; }; 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StepicTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 989FDF7763311650D54A4A11 /* SettingsStepFontSizeViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsStepFontSizeViewController.swift; sourceTree = ""; }; + 995EDC0C4A4B7085EE7589C2 /* WriteCommentOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentOutputProtocol.swift; sourceTree = ""; }; 9F98579F526E5A4D162C3356 /* Pods_Stepic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Stepic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A1F285E88F0E449B536C9DE9 /* NewCodeQuizFullscreenDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenDataFlow.swift; sourceTree = ""; }; A458B834FD5BF8E1E16E5A0A /* NewDiscussionsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewDiscussionsAssembly.swift; sourceTree = ""; }; A484FA9C0CDF08A193A71381 /* NewMatchingQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewMatchingQuizInteractor.swift; sourceTree = ""; }; + AACBF39DAC4A4A599F8949F2 /* WriteCommentInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentInteractor.swift; sourceTree = ""; }; AC5B5698955B5FF2B7EFB6F4 /* NewLessonDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonDataFlow.swift; sourceTree = ""; }; ACDDB66D0848F4DCD752795B /* NewStringQuizDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStringQuizDataFlow.swift; sourceTree = ""; }; AF89927A843447A9AE4C0AF4 /* NewChoiceQuizPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewChoiceQuizPresenter.swift; sourceTree = ""; }; @@ -2118,9 +2131,12 @@ BF5C47F13975D3915692AA71 /* NewSortingQuizDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewSortingQuizDataFlow.swift; sourceTree = ""; }; BFB28C8741D8E5CBF35B3437 /* WriteCourseReviewViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCourseReviewViewController.swift; sourceTree = ""; }; C6F7667856CC2C2A2AC43631 /* NewFreeAnswerQuizView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewFreeAnswerQuizView.swift; sourceTree = ""; }; + C77A40A485EDD8A3E4AA0157 /* WriteCommentProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentProvider.swift; sourceTree = ""; }; D109E72D69B31373C97237F8 /* Pods-Stepic.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.release.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.release.xcconfig"; sourceTree = ""; }; D3AF52F67CF39A1A5DC20F44 /* UnsupportedQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UnsupportedQuizInteractor.swift; sourceTree = ""; }; + D44BBB1022CD4FB280EB7DFB /* WriteCommentPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentPresenter.swift; sourceTree = ""; }; D779F899F835A1724F0C1186 /* NewMatchingQuizPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewMatchingQuizPresenter.swift; sourceTree = ""; }; + D7DBF5A43025E726B6D61E1E /* WriteCommentDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentDataFlow.swift; sourceTree = ""; }; DA4737058E519D801DAB2131 /* NewStepPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepPresenter.swift; sourceTree = ""; }; DC193B8E01FF4A61441ABF13 /* NewLessonViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonViewController.swift; sourceTree = ""; }; DCCE000FB8689122DC202C3E /* NewCodeQuizFullscreenOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenOutputProtocol.swift; sourceTree = ""; }; @@ -2405,7 +2421,6 @@ 62E9809004D6D03BB53BFCD7 /* DiscussionsTableViewDataSource.swift */, 2C6C17BA22CF412900FE776A /* DiscussionsView.swift */, 082CB1B41D08971900C79A27 /* DiscussionsViewController.swift */, - 08EB85EE1D101D7800E4F345 /* WriteCommentViewController.swift */, 082CB1B51D08971900C79A27 /* DiscussionsViewController.xib */, 2CD2605322CCD0C40054F01B /* Views */, ); @@ -3513,6 +3528,30 @@ path = NewMatchingQuiz; sourceTree = ""; }; + 257195BBF7205219EE894C00 /* WriteComment */ = { + isa = PBXGroup; + children = ( + 36F53BF6D2A309CFC55A72D5 /* WriteCommentAssembly.swift */, + D7DBF5A43025E726B6D61E1E /* WriteCommentDataFlow.swift */, + AACBF39DAC4A4A599F8949F2 /* WriteCommentInteractor.swift */, + D44BBB1022CD4FB280EB7DFB /* WriteCommentPresenter.swift */, + C77A40A485EDD8A3E4AA0157 /* WriteCommentProvider.swift */, + 1C65429C78822944952072FB /* WriteCommentView.swift */, + 4CCE255730E82181FAAF0AAA /* WriteCommentViewController.swift */, + 2C9C0E0E235F561A00EC31F3 /* WriteCommentViewModel.swift */, + 2B9378F3E9BF5C903F9F283E /* InputOutput */, + ); + path = WriteComment; + sourceTree = ""; + }; + 2B9378F3E9BF5C903F9F283E /* InputOutput */ = { + isa = PBXGroup; + children = ( + 995EDC0C4A4B7085EE7589C2 /* WriteCommentOutputProtocol.swift */, + ); + path = InputOutput; + sourceTree = ""; + }; 2C0176C32188A49100DDB9D0 /* Analytics */ = { isa = PBXGroup; children = ( @@ -4678,6 +4717,7 @@ 685E26390C2E037B1CD1C8A1 /* ProfileEdit */, 2CB273FE22C4E25C0078CA2F /* Quizzes */, 2C01BB69233D01E200C8DCF0 /* SettingsSubmodules */, + 257195BBF7205219EE894C00 /* WriteComment */, 6770E38E55D13B80C3F16A99 /* WriteCourseReview */, ); path = Modules; @@ -5889,6 +5929,7 @@ "${BUILT_PRODUCTS_DIR}/SVGKit/SVGKit.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", + "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", "${BUILT_PRODUCTS_DIR}/TSMessages/TSMessages.framework", @@ -5942,6 +5983,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVGKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftDate.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TSMessages.framework", @@ -6005,6 +6047,7 @@ "${BUILT_PRODUCTS_DIR}/SVGKit/SVGKit.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", + "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", "${BUILT_PRODUCTS_DIR}/TSMessages/TSMessages.framework", @@ -6054,6 +6097,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVGKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftDate.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TSMessages.framework", @@ -6401,7 +6445,6 @@ 088E73EA2060124B00D458E3 /* ApiRequestRetrier.swift in Sources */, 2CF08864205BEF3C00FCB9C0 /* StepikPlaceholderView.swift in Sources */, 2C6E9CDB1FF27543001821A2 /* AdaptiveStorageManager.swift in Sources */, - 08EB85F01D101D7800E4F345 /* WriteCommentViewController.swift in Sources */, 08AEC3A71CCA75F400FFF29E /* APIDefaults.swift in Sources */, 080CE1611E9581960089A27F /* SubmissionsAPI.swift in Sources */, 081387E11D7AF7700092E05D /* StyledTabBarViewController.swift in Sources */, @@ -6456,6 +6499,7 @@ 0860D9191F115D830087D61B /* CodeSuggestionsTableViewController.swift in Sources */, 084156941BCBFFBD006B8C73 /* Block.swift in Sources */, 084BD9BB1E5368B600B1901E /* PickerViewController.swift in Sources */, + 2C9C0E0F235F561A00EC31F3 /* WriteCommentViewModel.swift in Sources */, 08484F0C211AF4320006266F /* StoryViewController.swift in Sources */, 084472061D05918E00197166 /* ChoiceQuizTableViewCell.swift in Sources */, 083B164D1C2AF27700250B37 /* JSQWebViewController.swift in Sources */, @@ -6991,6 +7035,14 @@ 22B4950F115BC4B44A6CF64A /* NewDiscussionsDataFlow.swift in Sources */, BD98999E70D8AE972B458DB5 /* NewDiscussionsProvider.swift in Sources */, 3DE8B8A99E32EB6B11246C34 /* NewDiscussionsView.swift in Sources */, + 0E3927B3DADE084DA71C868B /* WriteCommentAssembly.swift in Sources */, + 7060C8E8055EC26669A7051B /* WriteCommentViewController.swift in Sources */, + 0C97AFADC2099B3785D9B910 /* WriteCommentInteractor.swift in Sources */, + 49D8E231DE76283BD37FFD3B /* WriteCommentPresenter.swift in Sources */, + A0A7AC991F92BEC73820B298 /* WriteCommentDataFlow.swift in Sources */, + 2D6AB32933F1C93FCF849056 /* WriteCommentProvider.swift in Sources */, + 7D334A7F2599276A266E17AE /* WriteCommentView.swift in Sources */, + 10F32F9073D41A146E8A3994 /* WriteCommentOutputProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7343,7 +7395,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 156; + CURRENT_PROJECT_VERSION = 157; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -7373,7 +7425,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 156; + CURRENT_PROJECT_VERSION = 157; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Analytics/AnalyticsEvents.swift b/Stepic/Analytics/AnalyticsEvents.swift index fbacf85ebf..13630a8a1b 100644 --- a/Stepic/Analytics/AnalyticsEvents.swift +++ b/Stepic/Analytics/AnalyticsEvents.swift @@ -120,6 +120,7 @@ struct AnalyticsEvents { static let liked = "discussion_liked" static let unliked = "discussion_unliked" static let abused = "discussion_abused" + static let unabused = "discussion_unabused" } struct DeepLink { diff --git a/Stepic/AttemptsAPI.swift b/Stepic/AttemptsAPI.swift index e193329cb8..5c777bbdc4 100644 --- a/Stepic/AttemptsAPI.swift +++ b/Stepic/AttemptsAPI.swift @@ -17,11 +17,12 @@ final class AttemptsAPI: APIEndpoint { func create(stepName: String, stepId: Int) -> Promise { let attempt = Attempt(step: stepId) return Promise { seal in - create.request(requestEndpoint: "attempts", paramName: "attempt", creatingObject: attempt, withManager: manager).done { attempt, json in - guard let json = json else { - seal.fulfill(attempt) - return - } + self.create.request( + requestEndpoint: "attempts", + paramName: "attempt", + creatingObject: attempt, + withManager: manager + ).done { attempt, json in attempt.initDataset(json: json["attempts"].arrayValue[0]["dataset"], stepName: stepName) seal.fulfill(attempt) }.catch { error in diff --git a/Stepic/CardsStepsViewController.swift b/Stepic/CardsStepsViewController.swift index 3f16602978..ff22071759 100644 --- a/Stepic/CardsStepsViewController.swift +++ b/Stepic/CardsStepsViewController.swift @@ -100,10 +100,7 @@ class CardsStepsViewController: UIViewController, CardsStepsView, ControllerWith } func presentDiscussions(stepId: Int, discussionProxyId: String) { - let assembly = DiscussionsLegacyAssembly( - discussionProxyID: discussionProxyId, - stepID: stepId - ) + let assembly = NewDiscussionsAssembly(discussionProxyID: discussionProxyId, stepID: stepId) self.push(module: assembly.makeModule()) } diff --git a/Stepic/CommentsAPI.swift b/Stepic/CommentsAPI.swift index f7f11be8b6..6f39acc297 100644 --- a/Stepic/CommentsAPI.swift +++ b/Stepic/CommentsAPI.swift @@ -49,37 +49,47 @@ final class CommentsAPI: APIEndpoint { func create(_ comment: Comment) -> Promise { return Promise { seal in - create.request(requestEndpoint: "comments", paramName: "comment", creatingObject: comment, withManager: manager).done { comment, json in - guard let json = json else { - seal.fulfill(comment) - return - } - let userInfo = UserInfo(json: json["users"].arrayValue[0]) - let vote = Vote(json: json["votes"].arrayValue[0]) + self.create.request( + requestEndpoint: self.name, + paramName: "comment", + creatingObject: comment, + withManager: self.manager + ).done { comment, json in + let userInfo = UserInfo(json: json[Comment.JSONKey.users.rawValue].arrayValue[0]) + let vote = Vote(json: json[Comment.JSONKey.votes.rawValue].arrayValue[0]) + comment.userInfo = userInfo comment.vote = vote + seal.fulfill(comment) }.catch { error in seal.reject(error) } } } -} -extension CommentsAPI { - @available(*, deprecated, message: "Legacy method with callbacks") - @discardableResult func create(_ comment: Comment, success: @escaping (Comment) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { - create(comment).done { success($0) }.catch { errorHandler($0.localizedDescription) } - return nil - } + func update(_ comment: Comment) -> Promise { + return Promise { seal in + self.update.request( + requestEndpoint: self.name, + paramName: "comment", + updatingObject: comment, + withManager: self.manager + ).done { comment, json in + let userInfo = UserInfo(json: json[Comment.JSONKey.users.rawValue].arrayValue[0]) + let vote = Vote(json: json[Comment.JSONKey.votes.rawValue].arrayValue[0]) - @available(*, deprecated, message: "Legacy method with callbacks") - @discardableResult func retrieve(_ ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ([Comment]) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { - retrieve(ids: ids).done { comments in - success(comments) - }.catch { error in - errorHandler(error.localizedDescription) + comment.userInfo = userInfo + comment.vote = vote + + seal.fulfill(comment) + }.catch { error in + seal.reject(error) + } } - return nil + } + + func delete(commentID: Comment.IdType) -> Promise { + return self.delete.request(requestEndpoint: self.name, deletingId: commentID, withManager: self.manager) } } diff --git a/Stepic/CreateRequestMaker.swift b/Stepic/CreateRequestMaker.swift index c792054fda..28f3728fb8 100644 --- a/Stepic/CreateRequestMaker.swift +++ b/Stepic/CreateRequestMaker.swift @@ -12,14 +12,24 @@ import PromiseKit import SwiftyJSON final class CreateRequestMaker { - func request(requestEndpoint: String, paramName: String, creatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise<(T, JSON?)> { + func request( + requestEndpoint: String, + paramName: String, + creatingObject: T, + withManager manager: Alamofire.SessionManager + ) -> Promise<(T, JSON)> { return Promise { seal in let params: Parameters? = [ paramName: creatingObject.json.dictionaryObject ?? "" ] checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", method: .post, parameters: params, encoding: JSONEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + method: .post, + parameters: params, + encoding: JSONEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) @@ -28,16 +38,25 @@ final class CreateRequestMaker { seal.fulfill((creatingObject, json)) } } - }.catch { - error in + }.catch { error in seal.reject(error) } } } - func request(requestEndpoint: String, paramName: String, creatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise { + func request( + requestEndpoint: String, + paramName: String, + creatingObject: T, + withManager manager: Alamofire.SessionManager + ) -> Promise { return Promise { seal in - request(requestEndpoint: requestEndpoint, paramName: paramName, creatingObject: creatingObject, withManager: manager).done { comment, _ in + self.request( + requestEndpoint: requestEndpoint, + paramName: paramName, + creatingObject: creatingObject, + withManager: manager + ).done { comment, _ in seal.fulfill(comment) }.catch { error in seal.reject(error) @@ -45,9 +64,19 @@ final class CreateRequestMaker { } } - func request(requestEndpoint: String, paramName: String, creatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise { + func request( + requestEndpoint: String, + paramName: String, + creatingObject: T, + withManager manager: Alamofire.SessionManager + ) -> Promise { return Promise { seal in - request(requestEndpoint: requestEndpoint, paramName: paramName, creatingObject: creatingObject, withManager: manager).done { _, _ in + self.request( + requestEndpoint: requestEndpoint, + paramName: paramName, + creatingObject: creatingObject, + withManager: manager + ).done { _, _ in seal.fulfill(()) }.catch { error in seal.reject(error) diff --git a/Stepic/DeleteRequestMaker.swift b/Stepic/DeleteRequestMaker.swift index 5f5165eaf1..cae3238717 100644 --- a/Stepic/DeleteRequestMaker.swift +++ b/Stepic/DeleteRequestMaker.swift @@ -11,10 +11,18 @@ import Foundation import PromiseKit final class DeleteRequestMaker { - func request(requestEndpoint: String, deletingId: Int, withManager manager: Alamofire.SessionManager) -> Promise { + func request( + requestEndpoint: String, + deletingId: Int, + withManager manager: Alamofire.SessionManager + ) -> Promise { return Promise { seal in checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(deletingId)", method: .delete, encoding: JSONEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(deletingId)", + method: .delete, + encoding: JSONEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) @@ -22,8 +30,7 @@ final class DeleteRequestMaker { seal.fulfill(()) } } - }.catch { - error in + }.catch { error in seal.reject(error) } } diff --git a/Stepic/Discussions/Comment.swift b/Stepic/Discussions/Comment.swift index 1e5430c6a7..8e25f5951b 100644 --- a/Stepic/Discussions/Comment.swift +++ b/Stepic/Discussions/Comment.swift @@ -16,7 +16,7 @@ enum UserRole: String { } final class Comment: JSONSerializable { - var id: Int = 0 + var id: Int = -1 var parentID: Comment.IdType? var userID: User.IdType = 0 var userRole: UserRole = .student @@ -31,6 +31,7 @@ final class Comment: JSONSerializable { var voteID: String = "" var epicCount: Int = 0 var abuseCount: Int = 0 + var actions: [Action] = [] var userInfo: UserInfo! var vote: Vote! @@ -78,6 +79,25 @@ final class Comment: JSONSerializable { self.voteID = json[JSONKey.vote.rawValue].stringValue self.epicCount = json[JSONKey.epicCount.rawValue].intValue self.abuseCount = json[JSONKey.abuseCount.rawValue].intValue + + self.actions.removeAll(keepingCapacity: true) + for (actionKey, value) in json[JSONKey.actions.rawValue].dictionaryValue { + guard let action = Action(rawValue: actionKey) else { + continue + } + + if value.boolValue { + self.actions.append(action) + } + } + } + + enum Action: String { + case delete + case pin + case report + case vote + case edit } enum JSONKey: String { @@ -96,6 +116,7 @@ final class Comment: JSONSerializable { case vote case epicCount = "epic_count" case abuseCount = "abuse_count" + case actions case users case votes } diff --git a/Stepic/DiscussionsStoryboard.storyboard b/Stepic/DiscussionsStoryboard.storyboard index f1c932be50..52dff30bf5 100644 --- a/Stepic/DiscussionsStoryboard.storyboard +++ b/Stepic/DiscussionsStoryboard.storyboard @@ -1,19 +1,19 @@ - + - + - + - + diff --git a/Stepic/DiscussionsViewController.swift b/Stepic/DiscussionsViewController.swift index a80bd12455..c1ace57b69 100644 --- a/Stepic/DiscussionsViewController.swift +++ b/Stepic/DiscussionsViewController.swift @@ -33,7 +33,7 @@ final class DiscussionsLegacyAssembly: Assembly { votesNetworkService: VotesNetworkService(votesAPI: VotesAPI()), stepsPersistenceService: StepsPersistenceService() ) - viewController.title = NSLocalizedString("Discussions", comment: "") + viewController.title = NSLocalizedString("DiscussionsTitle", comment: "") return viewController } } @@ -189,8 +189,6 @@ final class DiscussionsViewController: UIViewController, DiscussionsView, Contro } func displayWriteComment(parentId: Comment.IdType?) { - let assembly = WriteCommentLegacyAssembly(target: self.target, parentId: parentId, delegate: self) - self.push(module: assembly.makeModule()) } @objc @@ -215,9 +213,3 @@ extension DiscussionsViewController: DiscussionsViewControllerDelegate { self.push(module: assembly.makeModule()) } } - -extension DiscussionsViewController: WriteCommentViewControllerDelegate { - func writeCommentViewControllerDidWriteComment(_ controller: WriteCommentViewController, comment: Comment) { - self.presenter?.writeComment(comment) - } -} diff --git a/Stepic/Images.xcassets/New discussions/discussions-sort.imageset/Contents.json b/Stepic/Images.xcassets/New discussions/discussions-sort.imageset/Contents.json new file mode 100644 index 0000000000..3eccab0b18 --- /dev/null +++ b/Stepic/Images.xcassets/New discussions/discussions-sort.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "discussions-sort.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Stepic/Images.xcassets/New discussions/discussions-sort.imageset/discussions-sort.pdf b/Stepic/Images.xcassets/New discussions/discussions-sort.imageset/discussions-sort.pdf new file mode 100644 index 0000000000..7b4d278903 Binary files /dev/null and b/Stepic/Images.xcassets/New discussions/discussions-sort.imageset/discussions-sort.pdf differ diff --git a/Stepic/Info.plist b/Stepic/Info.plist index 979c0e4f2c..8709681670 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.98 + 1.99 CFBundleSignature ???? CFBundleURLTypes @@ -56,7 +56,7 @@ CFBundleVersion - 156 + 157 Fabric APIKey diff --git a/Stepic/RetrieveRequestMaker.swift b/Stepic/RetrieveRequestMaker.swift index d822cd0ab0..37d719b1b0 100644 --- a/Stepic/RetrieveRequestMaker.swift +++ b/Stepic/RetrieveRequestMaker.swift @@ -12,10 +12,20 @@ import PromiseKit import SwiftyJSON final class RetrieveRequestMaker { - func request(requestEndpoint: String, paramName: String, id: T.IdType, updatingObject: T? = nil, withManager manager: Alamofire.SessionManager) -> Promise { + func request( + requestEndpoint: String, + paramName: String, + id: T.IdType, + updatingObject: T? = nil, + withManager manager: Alamofire.SessionManager + ) -> Promise { return Promise { seal in checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(id)", method: .get, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(id)", + method: .get, + encoding: URLEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) @@ -27,24 +37,33 @@ final class RetrieveRequestMaker { } } } - }.catch { - error in + }.catch { error in seal.reject(error) } } } - func request(requestEndpoint: String, paramName: String, params: Parameters, updatingObjects: [T] = [], withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta, JSON)> { + func request( + requestEndpoint: String, + paramName: String, + params: Parameters, + updatingObjects: [T] = [], + withManager manager: Alamofire.SessionManager + ) -> Promise<([T], Meta, JSON)> { return Promise { seal in checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", method: .get, parameters: params, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + method: .get, + parameters: params, + encoding: URLEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) case .success(let json): let jsonArray: [JSON] = json[paramName].array ?? [] - let resultArray: [T] = jsonArray.map { - objectJSON in + let resultArray: [T] = jsonArray.map { objectJSON in if let recoveredIndex = updatingObjects.index(where: { $0.hasEqualId(json: objectJSON) }) { updatingObjects[recoveredIndex].update(json: objectJSON) return updatingObjects[recoveredIndex] @@ -57,40 +76,57 @@ final class RetrieveRequestMaker { CoreDataHelper.instance.save() } } - }.catch { - error in + }.catch { error in seal.reject(error) } } } - func request(requestEndpoint: String, paramName: String, params: Parameters, updatingObjects: [T] = [], withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta)> { + func request( + requestEndpoint: String, + paramName: String, + params: Parameters, + updatingObjects: [T] = [], + withManager manager: Alamofire.SessionManager + ) -> Promise<([T], Meta)> { return Promise { seal in - request(requestEndpoint: requestEndpoint, paramName: paramName, params: params, updatingObjects: updatingObjects, withManager: manager).done { - objects, meta, _ in + self.request( + requestEndpoint: requestEndpoint, + paramName: paramName, + params: params, + updatingObjects: updatingObjects, + withManager: manager + ).done { objects, meta, _ in seal.fulfill((objects, meta)) - }.catch { - error in + }.catch { error in seal.reject(error) } } } - func requestWithFetching(requestEndpoint: String, paramName: String, params: Parameters, withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta, JSON)> { + func requestWithFetching( + requestEndpoint: String, + paramName: String, + params: Parameters, + withManager manager: Alamofire.SessionManager + ) -> Promise<([T], Meta, JSON)> { return Promise { seal in checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", method: .get, parameters: params, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + method: .get, + parameters: params, + encoding: URLEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) case .success(let json): let ids = json[paramName].arrayValue.compactMap { T.getId(json: $0) } - T.fetchAsync(ids: ids).done { - existingObjects in + T.fetchAsync(ids: ids).done { existingObjects in var resultArray: [T] = [] for objectJSON in json[paramName].arrayValue { let existing = existingObjects.filter { obj in obj.hasEqualId(json: objectJSON) } - switch existing.count { case 0: resultArray.append(T(json: objectJSON)) @@ -106,42 +142,58 @@ final class RetrieveRequestMaker { let meta = Meta(json: json["meta"]) seal.fulfill((resultArray, meta, json)) CoreDataHelper.instance.save() - }.catch { - error in + }.catch { error in seal.reject(error) } } } - }.catch { - error in + }.catch { error in seal.reject(error) } } } - func requestWithFetching(requestEndpoint: String, paramName: String, params: Parameters, withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta)> { + func requestWithFetching( + requestEndpoint: String, + paramName: String, + params: Parameters, + withManager manager: Alamofire.SessionManager + ) -> Promise<([T], Meta)> { return Promise { seal in - requestWithFetching(requestEndpoint: requestEndpoint, paramName: paramName, params: params, withManager: manager).done { - objects, meta, _ in + self.requestWithFetching( + requestEndpoint: requestEndpoint, + paramName: paramName, + params: params, + withManager: manager + ).done { objects, meta, _ in seal.fulfill((objects, meta)) - }.catch { - error in + }.catch { error in seal.reject(error) } } } - func request(requestEndpoint: String, paramName: String, ids: [T.IdType], updating: [T], withManager manager: Alamofire.SessionManager) -> Promise<([T], JSON)> { + func request( + requestEndpoint: String, + paramName: String, + ids: [T.IdType], + updating: [T], + withManager manager: Alamofire.SessionManager + ) -> Promise<([T], JSON)> { let params: Parameters = [ "ids": ids ] + return Promise { seal in checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", parameters: params, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + parameters: params, + encoding: URLEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) - case .success(let json): let jsonArray: [JSON] = json[paramName].array ?? [] let resultArray: [T] = jsonArray.map { @@ -164,13 +216,23 @@ final class RetrieveRequestMaker { } } - func request(requestEndpoint: String, paramName: String, ids: [T.IdType], updating: [T], withManager manager: Alamofire.SessionManager) -> Promise<[T]> { + func request( + requestEndpoint: String, + paramName: String, + ids: [T.IdType], + updating: [T], + withManager manager: Alamofire.SessionManager + ) -> Promise<[T]> { return Promise { seal in - request(requestEndpoint: requestEndpoint, paramName: paramName, ids: ids, updating: updating, withManager: manager).done { - objects, _ in + self.request( + requestEndpoint: requestEndpoint, + paramName: paramName, + ids: ids, + updating: updating, + withManager: manager + ).done { objects, _ in seal.fulfill(objects) - }.catch { - error in + }.catch { error in seal.reject(error) } } diff --git a/Stepic/Services/DeepLinks/DeepLinkRouter.swift b/Stepic/Services/DeepLinks/DeepLinkRouter.swift index 44d2292cda..259339669c 100644 --- a/Stepic/Services/DeepLinks/DeepLinkRouter.swift +++ b/Stepic/Services/DeepLinks/DeepLinkRouter.swift @@ -305,7 +305,7 @@ final class DeepLinkRouter { } if let discussionProxyID = step.discussionProxyId { - let assembly = DiscussionsLegacyAssembly( + let assembly = NewDiscussionsAssembly( discussionProxyID: discussionProxyID, stepID: step.id ) diff --git a/Stepic/Sources/Helpers/FormatterHelper.swift b/Stepic/Sources/Helpers/FormatterHelper.swift index 7ee5a97707..88ced9065b 100644 --- a/Stepic/Sources/Helpers/FormatterHelper.swift +++ b/Stepic/Sources/Helpers/FormatterHelper.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftDate enum FormatterHelper { /// Format number; 1000 -> "1K", 900 -> "900" @@ -106,4 +107,13 @@ enum FormatterHelper { return dateFormatter.string(from: date) } + + /// Format a date to a string representation relative to another reference date (default current). + static func dateToRelativeString(_ date: Date, referenceDate: Date = Date()) -> String { + return date.in(region: .UTC).toRelative( + since: DateInRegion(referenceDate, region: .UTC), + style: RelativeFormatter.defaultStyle(), + locale: Locales.current + ) + } } diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsAssembly.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsAssembly.swift index d893cec5a5..434fec2a77 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsAssembly.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsAssembly.swift @@ -14,7 +14,8 @@ final class NewDiscussionsAssembly: Assembly { discussionProxiesNetworkService: DiscussionProxiesNetworkService( discussionProxiesAPI: DiscussionProxiesAPI() ), - commentsNetworkService: CommentsNetworkService(commentsAPI: CommentsAPI()) + commentsNetworkService: CommentsNetworkService(commentsAPI: CommentsAPI()), + votesNetworkService: VotesNetworkService(votesAPI: VotesAPI()) ) let presenter = NewDiscussionsPresenter() let interactor = NewDiscussionsInteractor( diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsDataFlow.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsDataFlow.swift index d412d9424a..43fba607cf 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsDataFlow.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsDataFlow.swift @@ -1,40 +1,29 @@ import Foundation enum NewDiscussions { - // MARK: Common structs + // MARK: Common types - struct DiscussionsResult { - let discussions: [NewDiscussionsDiscussionViewModel] - let discussionsLeftToLoad: Int - } - - struct DiscussionsData { + /// Interactor -> presenter + struct DiscussionsResponseData { let discussionProxy: DiscussionProxy let discussions: [Comment] let discussionsIDsFetchingMore: Set let replies: [Comment.IdType: [Comment]] - let sortType: SortType + let currentSortType: SortType } - enum SortType { + /// Presenter -> ViewController + struct DiscussionsViewData { + let discussions: [NewDiscussionsDiscussionViewModel] + let discussionsLeftToLoad: Int + } + + enum SortType: String, CaseIterable { case last case mostLiked case mostActive case recentActivity - var title: String { - switch self { - case .last: - return NSLocalizedString("DiscussionsSortTypeLastDiscussions", comment: "") - case .mostLiked: - return NSLocalizedString("DiscussionsSortTypeMostLikedDiscussions", comment: "") - case .mostActive: - return NSLocalizedString("DiscussionsSortTypeMostActiveDiscussions", comment: "") - case .recentActivity: - return NSLocalizedString("DiscussionsSortTypeRecentActivityDiscussions", comment: "") - } - } - static var `default`: SortType { return .last } @@ -47,7 +36,7 @@ enum NewDiscussions { struct Request { } struct Response { - let result: Result + let result: Result } struct ViewModel { @@ -60,7 +49,7 @@ enum NewDiscussions { struct Request { } struct Response { - let result: Result + let result: Result } struct ViewModel { @@ -75,43 +64,139 @@ enum NewDiscussions { } struct Response { - let result: DiscussionsData + let result: DiscussionsResponseData } struct ViewModel { - let data: DiscussionsResult + let data: DiscussionsViewData } } /// Present write course review (after compose bar button item click) enum WriteCommentPresentation { + enum PresentationContext { + case create + case edit + } + struct Request { let commentID: Comment.IdType? + let presentationContext: PresentationContext } struct Response { let targetID: Int let parentID: Comment.IdType? + let comment: Comment? + let presentationContext: PresentationContext } struct ViewModel { let targetID: Int let parentID: Comment.IdType? + let presentationContext: WriteComment.PresentationContext } } - /// Show current user newly created comment + /// Show newly created comment enum CommentCreated { + struct Response { + let result: DiscussionsResponseData + } + + struct ViewModel { + let data: DiscussionsViewData + } + } + + /// Show updated comment + enum CommentUpdated { + struct Response { + let result: DiscussionsResponseData + } + + struct ViewModel { + let data: DiscussionsViewData + } + } + + /// Deletes comment by id + enum CommentDelete { + struct Request { + let commentID: Comment.IdType + } + + struct Response { + let result: Result + } + + struct ViewModel { + let state: ViewControllerState + } + } + + /// Updates comment's vote value to epic or null + enum CommentLike { + struct Request { + let commentID: Comment.IdType + } + + struct Response { + let result: DiscussionsResponseData + } + + struct ViewModel { + let data: DiscussionsViewData + } + } + + /// Updates comment's vote value to abuse or null + enum CommentAbuse { + struct Request { + let commentID: Comment.IdType + } + + struct Response { + let result: DiscussionsResponseData + } + + struct ViewModel { + let data: DiscussionsViewData + } + } + + /// Presents sort action sheet (after sort type bar button item click) + enum SortTypePresentation { + struct Request { } + + struct Response { + let currentSortType: SortType + let availableSortTypes: [SortType] + } + + struct ViewModel { + let title: String + let items: [Item] + + struct Item { + let uniqueIdentifier: UniqueIdentifierType + let title: String + } + } + } + + /// Updates current sort type + enum SortTypeUpdate { struct Request { - let comment: Comment + let uniqueIdentifier: UniqueIdentifierType } struct Response { - let result: DiscussionsData + let result: DiscussionsResponseData } struct ViewModel { - let data: DiscussionsResult + let data: DiscussionsViewData } } @@ -131,11 +216,11 @@ enum NewDiscussions { enum ViewControllerState { case loading case error - case result(data: DiscussionsResult) + case result(data: DiscussionsViewData) } enum PaginationState { - case result(data: DiscussionsResult) + case result(data: DiscussionsViewData) case error } } diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsInteractor.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsInteractor.swift index dcf2355956..c4571a07f8 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsInteractor.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsInteractor.swift @@ -7,7 +7,11 @@ protocol NewDiscussionsInteractorProtocol { func doNextDiscussionsLoad(request: NewDiscussions.NextDiscussionsLoad.Request) func doNextRepliesLoad(request: NewDiscussions.NextRepliesLoad.Request) func doWriteCommentPresentation(request: NewDiscussions.WriteCommentPresentation.Request ) - func doCommentCreatedHandling(request: NewDiscussions.CommentCreated.Request) + func doCommentDelete(request: NewDiscussions.CommentDelete.Request) + func doCommentLike(request: NewDiscussions.CommentLike.Request) + func doCommentAbuse(request: NewDiscussions.CommentAbuse.Request) + func doSortTypePresentation(request: NewDiscussions.SortTypePresentation.Request) + func doSortTypeUpdate(request: NewDiscussions.SortTypeUpdate.Request) } final class NewDiscussionsInteractor: NewDiscussionsInteractorProtocol { @@ -45,7 +49,7 @@ final class NewDiscussionsInteractor: NewDiscussionsInteractorProtocol { return discussionProxy.discussionsIDsRecentActivity } } - private let currentSortType: NewDiscussions.SortType = .default + private var currentSortType: NewDiscussions.SortType = .default /// A Boolean value that determines whether the fetch of the replies for root discussion is in progress. private var discussionsIDsFetchingReplies: Set = [] @@ -70,6 +74,8 @@ final class NewDiscussionsInteractor: NewDiscussionsInteractorProtocol { // MARK: - NewDiscussionsInteractorProtocol - + // MARK: Fetching + func doDiscussionsLoad(request: NewDiscussions.DiscussionsLoad.Request) { self.fetchBackgroundQueue.async { [weak self] in guard let strongSelf = self else { @@ -186,67 +192,246 @@ final class NewDiscussionsInteractor: NewDiscussionsInteractorProtocol { } } + // MARK: Write & delete + func doWriteCommentPresentation(request: NewDiscussions.WriteCommentPresentation.Request) { - var parentID: Comment.IdType? + switch request.presentationContext { + case .create: + self.presentWriteComment(commentID: request.commentID) + case .edit: + if let commentID = request.commentID { + self.presentEditComment(commentID: commentID) + } else { + NewDiscussionsInteractor.logger.error( + "new discussions interactor: attempt to edit comment but comment id is nil" + ) + } + } + } - if let commentID = request.commentID { - parentID = { - if self.currentDiscussions.contains(where: { $0.id == commentID }) { - return commentID - } + func doCommentDelete(request: NewDiscussions.CommentDelete.Request) { + NewDiscussionsInteractor.logger.info( + "new discussions interactor: start deleting comment by id: \(request.commentID)" + ) + self.presenter.presentWaitingState( + response: NewDiscussions.BlockingWaitingIndicatorUpdate.Response(shouldDismiss: false) + ) + + let commentID = request.commentID + + self.provider.deleteComment(id: commentID).done { + NewDiscussionsInteractor.logger.info( + "new discussions interactor: successfully deleted comment with id: \(commentID)" + ) + + if let discussionIndex = self.currentDiscussions.firstIndex(where: { $0.id == commentID }) { + self.currentDiscussions.remove(at: discussionIndex) + self.currentReplies[commentID] = nil + } else { for (discussionID, replies) in self.currentReplies { - if discussionID == commentID { - return discussionID + guard let replyIndex = replies.firstIndex(where: { $0.id == commentID }) else { + continue } - if replies.contains(where: { $0.id == commentID }) { - return discussionID + + self.currentReplies[discussionID]?.remove(at: replyIndex) + if let discussionIndex = self.currentDiscussions.firstIndex(where: { $0.id == discussionID }) { + self.currentDiscussions[discussionIndex].repliesIDs.removeAll(where: { $0 == commentID }) } + break } - return nil - }() - } + } - self.presenter.presentWriteComment( - response: NewDiscussions.WriteCommentPresentation.Response(targetID: self.stepID, parentID: parentID) - ) + self.provider.fetchDiscussionProxy(id: self.discussionProxyID).done { discussionProxy in + self.currentDiscussionProxy = discussionProxy + }.ensure { + self.presenter.presentCommentDeleteResult( + response: NewDiscussions.CommentDelete.Response(result: .success(self.makeDiscussionsData())) + ) + }.cauterize() + }.catch { error in + NewDiscussionsInteractor.logger.info( + "new discussions interactor: failed delete comment with id: \(commentID)" + ) + self.presenter.presentCommentDeleteResult( + response: NewDiscussions.CommentDelete.Response(result: .failure(error)) + ) + } } - func doCommentCreatedHandling(request: NewDiscussions.CommentCreated.Request) { - let comment = request.comment + // MARK: Like & abuse - if let parentID = comment.parentID, - let parentIndex = self.currentDiscussions.firstIndex(where: { $0.id == parentID }) { - self.currentDiscussions[parentIndex].repliesIDs.append(comment.id) - self.currentReplies[parentID, default: []].append(comment) + func doCommentLike(request: NewDiscussions.CommentLike.Request) { + guard let comment = self.getAllComments().first(where: { $0.id == request.commentID }) else { + return NewDiscussionsInteractor.logger.error( + "new discussions interactor: unable to find comment: \(request.commentID)" + ) + } - self.presenter.presentCommentCreated( - response: NewDiscussions.CommentCreated.Response(result: self.makeDiscussionsData()) + if let voteValue = comment.vote.value { + let voteValueToSet: VoteValue? = voteValue == .epic ? nil : .epic + let vote = Vote(id: comment.vote.id, value: voteValueToSet) + + NewDiscussionsInteractor.logger.info( + "new discussions interactor: starting update vote from \(voteValue) to \(voteValueToSet ??? "null")" ) + + self.provider.updateVote(vote).done { vote in + NewDiscussionsInteractor.logger.info("new discussions interactor: successfully updated vote") + + comment.vote = vote + + switch voteValue { + case .abuse: + AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.liked) + comment.abuseCount -= 1 + comment.epicCount += 1 + case .epic: + AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.unliked) + comment.epicCount -= 1 + } + }.ensure { + self.presenter.presentCommentLikeResult( + response: NewDiscussions.CommentLike.Response(result: self.makeDiscussionsData()) + ) + }.catch { error in + NewDiscussionsInteractor.logger.error( + "new discussions interactor: failed update vote", + metadata: [ + "error": .string("\(error)"), + "commentID": .string("\(request.commentID)"), + "voteID": .string("\(vote.id)") + ] + ) + } } else { - self.presenter.presentWaitingState( - response: WriteCourseReview.BlockingWaitingIndicatorUpdate.Response(shouldDismiss: false) + NewDiscussionsInteractor.logger.info("new discussions interactor: starting update vote value to epic") + + let vote = Vote(id: comment.vote.id, value: .epic) + + self.provider.updateVote(vote).done { vote in + NewDiscussionsInteractor.logger.info("new discussions interactor: successfully updated vote") + AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.liked) + + comment.vote = vote + comment.epicCount += 1 + }.ensure { + self.presenter.presentCommentLikeResult( + response: NewDiscussions.CommentLike.Response(result: self.makeDiscussionsData()) + ) + }.catch { error in + NewDiscussionsInteractor.logger.error( + "new discussions interactor: failed update vote", + metadata: [ + "error": .string("\(error)"), + "commentID": .string("\(request.commentID)"), + "voteID": .string("\(vote.id)") + ] + ) + } + } + } + + func doCommentAbuse(request: NewDiscussions.CommentAbuse.Request) { + guard let comment = self.getAllComments().first(where: { $0.id == request.commentID }) else { + return NewDiscussionsInteractor.logger.error( + "new discussions interactor: unable to find comment: \(request.commentID)" ) + } - self.currentDiscussions.append(comment) + if let voteValue = comment.vote.value { + let voteValueToSet: VoteValue? = voteValue == .abuse ? nil : .abuse + let vote = Vote(id: comment.vote.id, value: voteValueToSet) - self.provider.fetchDiscussionProxy(id: self.discussionProxyID).done { discussionProxy in - self.currentDiscussionProxy = discussionProxy + NewDiscussionsInteractor.logger.info( + "new discussions interactor: starting update vote from \(voteValue) to \(voteValueToSet ??? "null")" + ) + + self.provider.updateVote(vote).done { vote in + NewDiscussionsInteractor.logger.info("new discussions interactor: successfully updated vote") + + comment.vote = vote + + switch voteValue { + case .abuse: + AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.unabused) + comment.abuseCount -= 1 + case .epic: + AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.abused) + comment.epicCount -= 1 + comment.abuseCount += 1 + } }.ensure { - self.presenter.presentWaitingState( - response: WriteCourseReview.BlockingWaitingIndicatorUpdate.Response(shouldDismiss: true) + self.presenter.presentCommentAbuseResult( + response: NewDiscussions.CommentAbuse.Response(result: self.makeDiscussionsData()) ) - self.presenter.presentCommentCreated( - response: NewDiscussions.CommentCreated.Response(result: self.makeDiscussionsData()) + }.catch { error in + NewDiscussionsInteractor.logger.error( + "new discussions interactor: failed update vote", + metadata: [ + "error": .string("\(error)"), + "commentID": .string("\(request.commentID)"), + "voteID": .string("\(vote.id)") + ] ) - }.cauterize() + } + } else { + NewDiscussionsInteractor.logger.info("new discussions interactor: starting update vote value to abuse") + + let vote = Vote(id: comment.vote.id, value: .abuse) + + self.provider.updateVote(vote).done { vote in + NewDiscussionsInteractor.logger.info("new discussions interactor: successfully updated vote") + AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.abused) + + comment.vote = vote + comment.abuseCount += 1 + }.ensure { + self.presenter.presentCommentAbuseResult( + response: NewDiscussions.CommentAbuse.Response(result: self.makeDiscussionsData()) + ) + }.catch { error in + NewDiscussionsInteractor.logger.error( + "new discussions interactor: failed update vote", + metadata: [ + "error": .string("\(error)"), + "commentID": .string("\(request.commentID)"), + "voteID": .string("\(vote.id)") + ] + ) + } } } + // MARK: Sort type + + func doSortTypePresentation(request: NewDiscussions.SortTypePresentation.Request) { + self.presenter.presentSortType( + response: NewDiscussions.SortTypePresentation.Response( + currentSortType: self.currentSortType, + availableSortTypes: NewDiscussions.SortType.allCases + ) + ) + } + + func doSortTypeUpdate(request: NewDiscussions.SortTypeUpdate.Request) { + guard let selectedSortType = NewDiscussions.SortType(rawValue: request.uniqueIdentifier), + self.currentSortType != selectedSortType else { + return + } + + self.currentSortType = selectedSortType + self.presenter.presentSortTypeUpdate( + response: NewDiscussions.SortTypeUpdate.Response(result: self.makeDiscussionsData()) + ) + } + // MARK: - Private API + // MARK: Fetching helpers + private func fetchDiscussions( discussionProxyID: DiscussionProxy.IdType - ) -> Promise { + ) -> Promise { // Reset data self.currentDiscussions = [] self.currentReplies = [:] @@ -289,13 +474,13 @@ final class NewDiscussionsInteractor: NewDiscussionsInteractorProtocol { } } - private func makeDiscussionsData() -> NewDiscussions.DiscussionsData { - return NewDiscussions.DiscussionsData( + private func makeDiscussionsData() -> NewDiscussions.DiscussionsResponseData { + return NewDiscussions.DiscussionsResponseData( discussionProxy: self.currentDiscussionProxy.require(), discussions: self.currentDiscussions, discussionsIDsFetchingMore: self.discussionsIDsFetchingReplies, replies: self.currentReplies, - sortType: self.currentSortType + currentSortType: self.currentSortType ) } @@ -326,9 +511,126 @@ final class NewDiscussionsInteractor: NewDiscussionsInteractorProtocol { return idsToLoad } + private func getAllComments() -> [Comment] { + return self.currentDiscussions + Array(self.currentReplies.values).reduce([], +) + } + + // MARK: Write & delete helpers + + private func presentWriteComment(commentID: Comment.IdType?) { + var parentID: Comment.IdType? + + if let commentID = commentID { + parentID = { + if self.currentDiscussions.contains(where: { $0.id == commentID }) { + return commentID + } + for (discussionID, replies) in self.currentReplies { + if discussionID == commentID { + return discussionID + } + if replies.contains(where: { $0.id == commentID }) { + return discussionID + } + } + return nil + }() + } + + self.presenter.presentWriteComment( + response: NewDiscussions.WriteCommentPresentation.Response( + targetID: self.stepID, + parentID: parentID, + comment: nil, + presentationContext: .create + ) + ) + } + + private func presentEditComment(commentID: Comment.IdType) { + let comment: Comment? = { + if let discussion = self.currentDiscussions.first(where: { $0.id == commentID }) { + return discussion + } + for (_, replies) in self.currentReplies { + if let reply = replies.first(where: { $0.id == commentID }) { + return reply + } + } + return nil + }() + + guard let unwrappedComment = comment else { + return NewDiscussionsInteractor.logger.error( + "new discussions interactor: attempt to edit comment but not able to find it by id" + ) + } + + self.presenter.presentWriteComment( + response: NewDiscussions.WriteCommentPresentation.Response( + targetID: self.stepID, + parentID: unwrappedComment.parentID, + comment: unwrappedComment, + presentationContext: .edit + ) + ) + } + // MARK: - Types - enum Error: Swift.Error { case fetchFailed } } + +// MARK: - NewDiscussionsInteractor: WriteCommentOutputProtocol - + +extension NewDiscussionsInteractor: WriteCommentOutputProtocol { + func handleCommentCreated(_ comment: Comment) { + if let parentID = comment.parentID, + let parentIndex = self.currentDiscussions.firstIndex(where: { $0.id == parentID }) { + self.currentDiscussions[parentIndex].repliesIDs.append(comment.id) + self.currentReplies[parentID, default: []].append(comment) + + self.presenter.presentCommentCreated( + response: NewDiscussions.CommentCreated.Response(result: self.makeDiscussionsData()) + ) + } else { + self.presenter.presentWaitingState( + response: NewDiscussions.BlockingWaitingIndicatorUpdate.Response(shouldDismiss: false) + ) + + self.currentDiscussions.append(comment) + + self.provider.fetchDiscussionProxy(id: self.discussionProxyID).done { discussionProxy in + self.currentDiscussionProxy = discussionProxy + }.ensure { + self.presenter.presentWaitingState( + response: NewDiscussions.BlockingWaitingIndicatorUpdate.Response(shouldDismiss: true) + ) + self.presenter.presentCommentCreated( + response: NewDiscussions.CommentCreated.Response(result: self.makeDiscussionsData()) + ) + }.cauterize() + } + } + + func handleCommentUpdated(_ comment: Comment) { + if let discussionIndex = self.currentDiscussions.firstIndex(where: { $0.id == comment.id }) { + self.currentDiscussions[discussionIndex] = comment + } else { + for (discussionID, replies) in self.currentReplies { + guard let replyIndex = replies.firstIndex(where: { $0.id == comment.id }) else { + continue + } + + self.currentReplies[discussionID]?[replyIndex] = comment + break + } + } + + self.presenter.presentCommentUpdated( + response: NewDiscussions.CommentUpdated.Response(result: self.makeDiscussionsData()) + ) + } +} diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsPresenter.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsPresenter.swift index c19bee2702..7e929ee412 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsPresenter.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsPresenter.swift @@ -6,7 +6,13 @@ protocol NewDiscussionsPresenterProtocol { func presentNextReplies(response: NewDiscussions.NextRepliesLoad.Response) func presentWriteComment(response: NewDiscussions.WriteCommentPresentation.Response) func presentCommentCreated(response: NewDiscussions.CommentCreated.Response) - func presentWaitingState(response: WriteCourseReview.BlockingWaitingIndicatorUpdate.Response) + func presentCommentUpdated(response: NewDiscussions.CommentUpdated.Response) + func presentCommentDeleteResult(response: NewDiscussions.CommentDelete.Response) + func presentCommentLikeResult(response: NewDiscussions.CommentLike.Response) + func presentCommentAbuseResult(response: NewDiscussions.CommentAbuse.Response) + func presentSortType(response: NewDiscussions.SortTypePresentation.Response) + func presentSortTypeUpdate(response: NewDiscussions.SortTypeUpdate.Response) + func presentWaitingState(response: NewDiscussions.BlockingWaitingIndicatorUpdate.Response) } final class NewDiscussionsPresenter: NewDiscussionsPresenterProtocol { @@ -17,13 +23,7 @@ final class NewDiscussionsPresenter: NewDiscussionsPresenterProtocol { switch response.result { case .success(let result): - let data = self.makeDiscussionsData( - discussionProxy: result.discussionProxy, - discussions: result.discussions, - discussionsIDsFetchingMore: result.discussionsIDsFetchingMore, - replies: result.replies, - sortType: result.sortType - ) + let data = self.makeDiscussionsData(result) viewModel = NewDiscussions.DiscussionsLoad.ViewModel(state: .result(data: data)) case .failure: viewModel = NewDiscussions.DiscussionsLoad.ViewModel(state: .error) @@ -37,13 +37,7 @@ final class NewDiscussionsPresenter: NewDiscussionsPresenterProtocol { switch response.result { case .success(let result): - let data = self.makeDiscussionsData( - discussionProxy: result.discussionProxy, - discussions: result.discussions, - discussionsIDsFetchingMore: result.discussionsIDsFetchingMore, - replies: result.replies, - sortType: result.sortType - ) + let data = self.makeDiscussionsData(result) viewModel = NewDiscussions.NextDiscussionsLoad.ViewModel(state: .result(data: data)) case .failure: viewModel = NewDiscussions.NextDiscussionsLoad.ViewModel(state: .error) @@ -53,77 +47,132 @@ final class NewDiscussionsPresenter: NewDiscussionsPresenterProtocol { } func presentNextReplies(response: NewDiscussions.NextRepliesLoad.Response) { - let data = self.makeDiscussionsData( - discussionProxy: response.result.discussionProxy, - discussions: response.result.discussions, - discussionsIDsFetchingMore: response.result.discussionsIDsFetchingMore, - replies: response.result.replies, - sortType: response.result.sortType - ) - + let data = self.makeDiscussionsData(response.result) self.viewController?.displayNextReplies(viewModel: NewDiscussions.NextRepliesLoad.ViewModel(data: data)) } func presentWriteComment(response: NewDiscussions.WriteCommentPresentation.Response) { + let presentationContext: WriteComment.PresentationContext = { + switch response.presentationContext { + case .create: + return .create + case .edit: + return .edit(response.comment.require()) + } + }() + self.viewController?.displayWriteComment( viewModel: NewDiscussions.WriteCommentPresentation.ViewModel( targetID: response.targetID, - parentID: response.parentID + parentID: response.parentID, + presentationContext: presentationContext ) ) } func presentCommentCreated(response: NewDiscussions.CommentCreated.Response) { - let data = self.makeDiscussionsData( - discussionProxy: response.result.discussionProxy, - discussions: response.result.discussions, - discussionsIDsFetchingMore: response.result.discussionsIDsFetchingMore, - replies: response.result.replies, - sortType: response.result.sortType + let data = self.makeDiscussionsData(response.result) + self.viewController?.displayCommentCreated(viewModel: NewDiscussions.CommentCreated.ViewModel(data: data)) + } + + func presentCommentUpdated(response: NewDiscussions.CommentUpdated.Response) { + let data = self.makeDiscussionsData(response.result) + self.viewController?.displayCommentUpdated(viewModel: NewDiscussions.CommentUpdated.ViewModel(data: data)) + } + + func presentCommentDeleteResult(response: NewDiscussions.CommentDelete.Response) { + switch response.result { + case .success(let data): + let viewModel = self.makeDiscussionsData(data) + self.viewController?.displayCommentDeleteResult( + viewModel: NewDiscussions.CommentDelete.ViewModel(state: .result(data: viewModel)) + ) + case .failure: + self.viewController?.displayCommentDeleteResult( + viewModel: NewDiscussions.CommentDelete.ViewModel(state: .error) + ) + } + } + + func presentCommentLikeResult(response: NewDiscussions.CommentLike.Response) { + let data = self.makeDiscussionsData(response.result) + self.viewController?.displayCommentLikeResult(viewModel: NewDiscussions.CommentLike.ViewModel(data: data)) + } + + func presentCommentAbuseResult(response: NewDiscussions.CommentAbuse.Response) { + let data = self.makeDiscussionsData(response.result) + self.viewController?.displayCommentAbuseResult(viewModel: NewDiscussions.CommentAbuse.ViewModel(data: data)) + } + + func presentSortType(response: NewDiscussions.SortTypePresentation.Response) { + let items = response.availableSortTypes.map { sortType -> NewDiscussions.SortTypePresentation.ViewModel.Item in + var title: String = { + switch sortType { + case .last: + return NSLocalizedString("DiscussionsSortTypeLastDiscussions", comment: "") + case .mostLiked: + return NSLocalizedString("DiscussionsSortTypeMostLikedDiscussions", comment: "") + case .mostActive: + return NSLocalizedString("DiscussionsSortTypeMostActiveDiscussions", comment: "") + case .recentActivity: + return NSLocalizedString("DiscussionsSortTypeRecentActivityDiscussions", comment: "") + } + }() + + if sortType == response.currentSortType { + title = "\(title) ✔︎" + } + + return .init(uniqueIdentifier: sortType.rawValue, title: title) + } + + self.viewController?.displaySortTypeAlert( + viewModel: NewDiscussions.SortTypePresentation.ViewModel( + title: NSLocalizedString("DiscussionsSortTypeAlertTitle", comment: ""), + items: items + ) ) + } - self.viewController?.displayCommentCreated( - viewModel: NewDiscussions.CommentCreated.ViewModel(data: data) + func presentSortTypeUpdate(response: NewDiscussions.SortTypeUpdate.Response) { + self.viewController?.displaySortTypeUpdate( + viewModel: NewDiscussions.SortTypeUpdate.ViewModel(data: self.makeDiscussionsData(response.result)) ) } - func presentWaitingState(response: WriteCourseReview.BlockingWaitingIndicatorUpdate.Response) { + func presentWaitingState(response: NewDiscussions.BlockingWaitingIndicatorUpdate.Response) { self.viewController?.displayBlockingLoadingIndicator( - viewModel: WriteCourseReview.BlockingWaitingIndicatorUpdate.ViewModel(shouldDismiss: response.shouldDismiss) + viewModel: NewDiscussions.BlockingWaitingIndicatorUpdate.ViewModel(shouldDismiss: response.shouldDismiss) ) } // MARK: - Private API - private func makeDiscussionsData( - discussionProxy: DiscussionProxy, - discussions: [Comment], - discussionsIDsFetchingMore: Set, - replies: [Comment.IdType: [Comment]], - sortType: NewDiscussions.SortType - ) -> NewDiscussions.DiscussionsResult { - assert(discussions.filter({ !$0.repliesIDs.isEmpty }).count == replies.keys.count) + _ data: NewDiscussions.DiscussionsResponseData + ) -> NewDiscussions.DiscussionsViewData { + assert(data.discussions.filter({ !$0.repliesIDs.isEmpty }).count == data.replies.keys.count) let discussions = self.sortedDiscussions( - discussions, - discussionProxy: discussionProxy, - by: sortType + data.discussions, + discussionProxy: data.discussionProxy, + by: data.currentSortType ) let discussionsViewModels = discussions.map { discussion in self.makeDiscussionViewModel( discussion: discussion, - replies: replies[discussion.id] ?? [], - isFetchingMoreReplies: discussionsIDsFetchingMore.contains(discussion.id) + replies: data.replies[discussion.id] ?? [], + isFetchingMoreReplies: data.discussionsIDsFetchingMore.contains(discussion.id) ) } let discussionsLeftToLoad = self.getDiscussionsIDs( - discussionProxy: discussionProxy, - sortType: sortType + discussionProxy: data.discussionProxy, + sortType: data.currentSortType ).count - discussions.count - return NewDiscussions.DiscussionsResult( + return NewDiscussions.DiscussionsViewData( discussions: discussionsViewModels, discussionsLeftToLoad: discussionsLeftToLoad ) @@ -144,7 +193,7 @@ final class NewDiscussionsPresenter: NewDiscussionsPresenterProtocol { return "Unknown" }() - let dateRepresentation = comment.time.getStepicFormatString(withTime: true) + let dateRepresentation = FormatterHelper.dateToRelativeString(comment.time) let voteValue: VoteValue? = { if let vote = comment.vote { @@ -163,7 +212,10 @@ final class NewDiscussionsPresenter: NewDiscussionsPresenterProtocol { dateRepresentation: dateRepresentation, likesCount: comment.epicCount, dislikesCount: comment.abuseCount, - voteValue: voteValue + voteValue: voteValue, + canEdit: comment.actions.contains(.edit), + canDelete: comment.actions.contains(.delete), + canVote: comment.actions.contains(.vote) ) } diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsProvider.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsProvider.swift index 077d8bf2a5..d25c947b63 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsProvider.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsProvider.swift @@ -4,18 +4,23 @@ import PromiseKit protocol NewDiscussionsProviderProtocol { func fetchDiscussionProxy(id: DiscussionProxy.IdType) -> Promise func fetchComments(ids: [Comment.IdType]) -> Promise<[Comment]> + func deleteComment(id: Comment.IdType) -> Promise + func updateVote(_ vote: Vote) -> Promise } final class NewDiscussionsProvider: NewDiscussionsProviderProtocol { private let discussionProxiesNetworkService: DiscussionProxiesNetworkServiceProtocol private let commentsNetworkService: CommentsNetworkServiceProtocol + private let votesNetworkService: VotesNetworkServiceProtocol init( discussionProxiesNetworkService: DiscussionProxiesNetworkServiceProtocol, - commentsNetworkService: CommentsNetworkServiceProtocol + commentsNetworkService: CommentsNetworkServiceProtocol, + votesNetworkService: VotesNetworkServiceProtocol ) { self.discussionProxiesNetworkService = discussionProxiesNetworkService self.commentsNetworkService = commentsNetworkService + self.votesNetworkService = votesNetworkService } func fetchDiscussionProxy(id: DiscussionProxy.IdType) -> Promise { @@ -38,7 +43,29 @@ final class NewDiscussionsProvider: NewDiscussionsProviderProtocol { } } + func deleteComment(id: Comment.IdType) -> Promise { + return Promise { seal in + self.commentsNetworkService.delete(id: id).done { + seal.fulfill(()) + }.catch { _ in + seal.reject(Error.commentDeleteFailed) + } + } + } + + func updateVote(_ vote: Vote) -> Promise { + return Promise { seal in + self.votesNetworkService.update(vote: vote).done { vote in + seal.fulfill(vote) + }.catch { _ in + seal.reject(Error.voteUpdateFailed) + } + } + } + enum Error: Swift.Error { case fetchFailed + case commentDeleteFailed + case voteUpdateFailed } } diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewController.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewController.swift index 58b807325a..2016c12cb4 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewController.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewController.swift @@ -7,7 +7,13 @@ protocol NewDiscussionsViewControllerProtocol: class { func displayNextReplies(viewModel: NewDiscussions.NextRepliesLoad.ViewModel) func displayWriteComment(viewModel: NewDiscussions.WriteCommentPresentation.ViewModel) func displayCommentCreated(viewModel: NewDiscussions.CommentCreated.ViewModel) - func displayBlockingLoadingIndicator(viewModel: WriteCourseReview.BlockingWaitingIndicatorUpdate.ViewModel) + func displayCommentUpdated(viewModel: NewDiscussions.CommentUpdated.ViewModel) + func displayCommentDeleteResult(viewModel: NewDiscussions.CommentDelete.ViewModel) + func displayCommentLikeResult(viewModel: NewDiscussions.CommentLike.ViewModel) + func displayCommentAbuseResult(viewModel: NewDiscussions.CommentAbuse.ViewModel) + func displaySortTypeAlert(viewModel: NewDiscussions.SortTypePresentation.ViewModel) + func displaySortTypeUpdate(viewModel: NewDiscussions.SortTypeUpdate.ViewModel) + func displayBlockingLoadingIndicator(viewModel: NewDiscussions.BlockingWaitingIndicatorUpdate.ViewModel) } final class NewDiscussionsViewController: UIViewController, ControllerWithStepikPlaceholder { @@ -26,6 +32,19 @@ final class NewDiscussionsViewController: UIViewController, ControllerWithStepik return tableDataSource }() + private lazy var sortTypeBarButtonItem = UIBarButtonItem( + image: UIImage(named: "discussions-sort")?.withRenderingMode(.alwaysTemplate), + style: .plain, + target: self, + action: #selector(self.didClickSortType) + ) + + private lazy var composeBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .compose, + target: self, + action: #selector(self.didClickWriteComment) + ) + init( interactor: NewDiscussionsInteractorProtocol, initialState: NewDiscussions.ViewControllerState = .loading @@ -49,14 +68,10 @@ final class NewDiscussionsViewController: UIViewController, ControllerWithStepik override func viewDidLoad() { super.viewDidLoad() - self.title = NSLocalizedString("Discussions", comment: "") + self.title = NSLocalizedString("DiscussionsTitle", comment: "") self.registerPlaceholders() - self.navigationItem.rightBarButtonItem = UIBarButtonItem( - barButtonSystemItem: .compose, - target: self, - action: #selector(self.didClickWriteComment) - ) + self.navigationItem.rightBarButtonItems = [self.composeBarButtonItem, self.sortTypeBarButtonItem] self.updateState(newState: self.state) self.interactor.doDiscussionsLoad(request: .init()) @@ -111,7 +126,7 @@ final class NewDiscussionsViewController: UIViewController, ControllerWithStepik } } - private func updateDiscussionsData(newData data: NewDiscussions.DiscussionsResult) { + private func updateDiscussionsData(newData data: NewDiscussions.DiscussionsViewData) { if data.discussions.isEmpty { self.showPlaceholder(for: .empty) } else { @@ -135,7 +150,12 @@ final class NewDiscussionsViewController: UIViewController, ControllerWithStepik @objc private func didClickWriteComment() { - self.interactor.doWriteCommentPresentation(request: .init(commentID: nil)) + self.interactor.doWriteCommentPresentation(request: .init(commentID: nil, presentationContext: .create)) + } + + @objc + private func didClickSortType() { + self.interactor.doSortTypePresentation(request: .init()) } } @@ -160,19 +180,72 @@ extension NewDiscussionsViewController: NewDiscussionsViewControllerProtocol { } func displayWriteComment(viewModel: NewDiscussions.WriteCommentPresentation.ViewModel) { - let assembly = WriteCommentLegacyAssembly( - target: viewModel.targetID, - parentId: viewModel.parentID, - delegate: self + let assembly = WriteCommentAssembly( + targetID: viewModel.targetID, + parentID: viewModel.parentID, + presentationContext: viewModel.presentationContext, + output: self.interactor as? WriteCommentOutputProtocol ) - self.push(module: assembly.makeModule()) + let navigationController = StyledNavigationController(rootViewController: assembly.makeModule()) + self.present(navigationController, animated: true) } func displayCommentCreated(viewModel: NewDiscussions.CommentCreated.ViewModel) { self.updateDiscussionsData(newData: viewModel.data) } - func displayBlockingLoadingIndicator(viewModel: WriteCourseReview.BlockingWaitingIndicatorUpdate.ViewModel) { + func displayCommentUpdated(viewModel: NewDiscussions.CommentUpdated.ViewModel) { + self.updateDiscussionsData(newData: viewModel.data) + } + + func displayCommentDeleteResult(viewModel: NewDiscussions.CommentDelete.ViewModel) { + switch viewModel.state { + case .result(let data): + SVProgressHUD.showSuccess(withStatus: "") + self.updateDiscussionsData(newData: data) + case .error: + SVProgressHUD.showError(withStatus: "") + case .loading: + break + } + } + + func displayCommentLikeResult(viewModel: NewDiscussions.CommentLike.ViewModel) { + self.updateDiscussionsData(newData: viewModel.data) + } + + func displayCommentAbuseResult(viewModel: NewDiscussions.CommentAbuse.ViewModel) { + self.updateDiscussionsData(newData: viewModel.data) + } + + func displaySortTypeAlert(viewModel: NewDiscussions.SortTypePresentation.ViewModel) { + let alert = UIAlertController(title: viewModel.title, message: nil, preferredStyle: .actionSheet) + + viewModel.items.forEach { sortTypeItem in + let action = UIAlertAction( + title: sortTypeItem.title, + style: .default, + handler: { [weak self] _ in + self?.interactor.doSortTypeUpdate(request: .init(uniqueIdentifier: sortTypeItem.uniqueIdentifier)) + } + ) + alert.addAction(action) + } + + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil)) + + if let popoverPresentationController = alert.popoverPresentationController { + popoverPresentationController.barButtonItem = self.sortTypeBarButtonItem + } + + self.present(alert, animated: true) + } + + func displaySortTypeUpdate(viewModel: NewDiscussions.SortTypeUpdate.ViewModel) { + self.updateDiscussionsData(newData: viewModel.data) + } + + func displayBlockingLoadingIndicator(viewModel: NewDiscussions.BlockingWaitingIndicatorUpdate.ViewModel) { if viewModel.shouldDismiss { SVProgressHUD.dismiss() } else { @@ -218,10 +291,67 @@ extension NewDiscussionsViewController: NewDiscussionsViewDelegate { title: NSLocalizedString("Reply", comment: ""), style: .default, handler: { [weak self] _ in - self?.interactor.doWriteCommentPresentation(request: .init(commentID: viewModel.id)) + self?.interactor.doWriteCommentPresentation( + request: .init(commentID: viewModel.id, presentationContext: .create) + ) } ) ) + + if viewModel.canEdit { + alert.addAction( + UIAlertAction( + title: NSLocalizedString("DiscussionsAlertActionEditTitle", comment: ""), + style: .default, + handler: { [weak self] _ in + self?.interactor.doWriteCommentPresentation( + request: .init(commentID: viewModel.id, presentationContext: .edit) + ) + } + ) + ) + } + + if viewModel.canVote { + let likeTitle = viewModel.voteValue == .epic + ? NSLocalizedString("DiscussionsAlertActionUnlikeTitle", comment: "") + : NSLocalizedString("DiscussionsAlertActionLikeTitle", comment: "") + alert.addAction( + UIAlertAction( + title: likeTitle, + style: .default, + handler: { [weak self] _ in + self?.interactor.doCommentLike(request: .init(commentID: viewModel.id)) + } + ) + ) + + let abuseTitle = viewModel.voteValue == .abuse + ? NSLocalizedString("DiscussionsAlertActionUnabuseTitle", comment: "") + : NSLocalizedString("DiscussionsAlertActionAbuseTitle", comment: "") + alert.addAction( + UIAlertAction( + title: abuseTitle, + style: .default, + handler: { [weak self] _ in + self?.interactor.doCommentAbuse(request: .init(commentID: viewModel.id)) + } + ) + ) + } + + if viewModel.canDelete { + alert.addAction( + UIAlertAction( + title: NSLocalizedString("DiscussionsAlertActionDeleteTitle", comment: ""), + style: .destructive, + handler: { [weak self] _ in + self?.interactor.doCommentDelete(request: .init(commentID: viewModel.id)) + } + ) + ) + } + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil)) if let popoverPresentationController = alert.popoverPresentationController { @@ -233,14 +363,6 @@ extension NewDiscussionsViewController: NewDiscussionsViewDelegate { } } -// MARK: - NewDiscussionsViewController: WriteCommentViewControllerDelegate - - -extension NewDiscussionsViewController: WriteCommentViewControllerDelegate { - func writeCommentViewControllerDidWriteComment(_ controller: WriteCommentViewController, comment: Comment) { - self.interactor.doCommentCreatedHandling(request: NewDiscussions.CommentCreated.Request(comment: comment)) - } -} - // MARK: - NewDiscussionsViewController: NewDiscussionsTableViewDataSourceDelegate - extension NewDiscussionsViewController: NewDiscussionsTableViewDataSourceDelegate { @@ -248,6 +370,22 @@ extension NewDiscussionsViewController: NewDiscussionsTableViewDataSourceDelegat _ tableViewDataSource: NewDiscussionsTableViewDataSource, viewModel: NewDiscussionsCommentViewModel ) { - self.interactor.doWriteCommentPresentation(request: .init(commentID: viewModel.id)) + self.interactor.doWriteCommentPresentation( + request: .init(commentID: viewModel.id, presentationContext: .create) + ) + } + + func newDiscussionsTableViewDataSourceDidRequestLike( + _ tableViewDataSource: NewDiscussionsTableViewDataSource, + viewModel: NewDiscussionsCommentViewModel + ) { + self.interactor.doCommentLike(request: .init(commentID: viewModel.id)) + } + + func newDiscussionsTableViewDataSourceDidRequestDislike( + _ tableViewDataSource: NewDiscussionsTableViewDataSource, + viewModel: NewDiscussionsCommentViewModel + ) { + self.interactor.doCommentAbuse(request: .init(commentID: viewModel.id)) } } diff --git a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewModel.swift b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewModel.swift index 370d8a5b93..59b1f0bc3f 100644 --- a/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewModel.swift +++ b/Stepic/Sources/Modules/NewDiscussions/NewDiscussionsViewModel.swift @@ -24,4 +24,7 @@ struct NewDiscussionsCommentViewModel { let likesCount: Int let dislikesCount: Int let voteValue: VoteValue? + let canEdit: Bool + let canDelete: Bool + let canVote: Bool } diff --git a/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsCellView.swift b/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsCellView.swift index 2def8c17ab..f135ce1d80 100644 --- a/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsCellView.swift +++ b/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsCellView.swift @@ -86,6 +86,7 @@ final class NewDiscussionsCellView: UIView { label.font = self.appearance.dateLabelFont label.textColor = self.appearance.dateLabelTextColor label.numberOfLines = 1 + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return label }() @@ -106,6 +107,7 @@ final class NewDiscussionsCellView: UIView { imageButton.title = "0" imageButton.image = UIImage(named: "discussions-thumb-up")?.withRenderingMode(.alwaysTemplate) imageButton.titleInsets = self.appearance.likeButtonTitleInsets + imageButton.addTarget(self, action: #selector(self.likeDidClick), for: .touchUpInside) return imageButton }() @@ -117,6 +119,7 @@ final class NewDiscussionsCellView: UIView { imageButton.title = "0" imageButton.image = UIImage(named: "discussions-thumb-down")?.withRenderingMode(.alwaysTemplate) imageButton.titleInsets = self.appearance.dislikeButtonTitleInsets + imageButton.addTarget(self, action: #selector(self.dislikeDidClick), for: .touchUpInside) return imageButton }() @@ -135,6 +138,8 @@ final class NewDiscussionsCellView: UIView { private var nameLabelTopConstraint: Constraint? var onReplyClick: (() -> Void)? + var onLikeClick: (() -> Void)? + var onDislikeClick: (() -> Void)? init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { self.appearance = appearance @@ -158,7 +163,7 @@ final class NewDiscussionsCellView: UIView { self.nameLabel.text = nil self.dateLabel.text = nil self.textLabel.text = nil - self.updateVote(likes: 0, dislikes: 0, voteValue: nil) + self.updateVotes(likes: 0, dislikes: 0, voteValue: nil, canVote: false) self.avatarImageView.reset() return } @@ -172,7 +177,12 @@ final class NewDiscussionsCellView: UIView { self.updateBadge(text: NSLocalizedString("Staff", comment: ""), isHidden: false) } - self.updateVote(likes: viewModel.likesCount, dislikes: viewModel.dislikesCount, voteValue: viewModel.voteValue) + self.updateVotes( + likes: viewModel.likesCount, + dislikes: viewModel.dislikesCount, + voteValue: viewModel.voteValue, + canVote: viewModel.canVote + ) self.nameLabel.text = viewModel.userName self.dateLabel.text = viewModel.dateRepresentation @@ -193,7 +203,7 @@ final class NewDiscussionsCellView: UIView { self.nameLabelTopConstraint?.update(offset: isHidden ? 0 : self.appearance.nameLabelInsets.top) } - private func updateVote(likes: Int, dislikes: Int, voteValue: VoteValue?) { + private func updateVotes(likes: Int, dislikes: Int, voteValue: VoteValue?, canVote: Bool) { self.likeImageButton.title = "\(likes)" self.dislikeImageButton.title = "\(dislikes)" @@ -207,12 +217,25 @@ final class NewDiscussionsCellView: UIView { self.likeImageButton.tintColor = self.appearance.likeImageNormalTintColor self.dislikeImageButton.tintColor = self.appearance.likeImageNormalTintColor } + + self.likeImageButton.isEnabled = canVote + self.dislikeImageButton.isEnabled = canVote } @objc private func replyDidClick() { self.onReplyClick?() } + + @objc + private func likeDidClick() { + self.onLikeClick?() + } + + @objc + private func dislikeDidClick() { + self.onDislikeClick?() + } } // MARK: - NewDiscussionsCellView: ProgrammaticallyInitializableViewProtocol - diff --git a/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsTableViewCell.swift b/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsTableViewCell.swift index 717d82cefe..d5718cafe4 100644 --- a/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsTableViewCell.swift +++ b/Stepic/Sources/Modules/NewDiscussions/Views/Cell/NewDiscussionsTableViewCell.swift @@ -16,6 +16,12 @@ final class NewDiscussionsTableViewCell: UITableViewCell, Reusable { cellView.onReplyClick = { [weak self] in self?.onReplyClick?() } + cellView.onLikeClick = { [weak self] in + self?.onLikeClick?() + } + cellView.onDislikeClick = { [weak self] in + self?.onDislikeClick?() + } return cellView }() @@ -34,6 +40,8 @@ final class NewDiscussionsTableViewCell: UITableViewCell, Reusable { private var separatorType: ViewModel.SeparatorType = .small var onReplyClick: (() -> Void)? + var onLikeClick: (() -> Void)? + var onDislikeClick: (() -> Void)? override func updateConstraintsIfNeeded() { super.updateConstraintsIfNeeded() diff --git a/Stepic/Sources/Modules/NewDiscussions/Views/NewDiscussionsTableViewDataSource.swift b/Stepic/Sources/Modules/NewDiscussions/Views/NewDiscussionsTableViewDataSource.swift index 8467f1d8f9..b0109890d8 100644 --- a/Stepic/Sources/Modules/NewDiscussions/Views/NewDiscussionsTableViewDataSource.swift +++ b/Stepic/Sources/Modules/NewDiscussions/Views/NewDiscussionsTableViewDataSource.swift @@ -5,6 +5,14 @@ protocol NewDiscussionsTableViewDataSourceDelegate: class { _ tableViewDataSource: NewDiscussionsTableViewDataSource, viewModel: NewDiscussionsCommentViewModel ) + func newDiscussionsTableViewDataSourceDidRequestLike( + _ tableViewDataSource: NewDiscussionsTableViewDataSource, + viewModel: NewDiscussionsCommentViewModel + ) + func newDiscussionsTableViewDataSourceDidRequestDislike( + _ tableViewDataSource: NewDiscussionsTableViewDataSource, + viewModel: NewDiscussionsCommentViewModel + ) } final class NewDiscussionsTableViewDataSource: NSObject { @@ -112,6 +120,26 @@ extension NewDiscussionsTableViewDataSource: UITableViewDataSource { viewModel: commentViewModel ) } + cell.onLikeClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.newDiscussionsTableViewDataSourceDidRequestLike( + strongSelf, + viewModel: commentViewModel + ) + } + cell.onDislikeClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.newDiscussionsTableViewDataSourceDidRequestDislike( + strongSelf, + viewModel: commentViewModel + ) + } let isLastComment = indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - self.loadMoreRepliesInset(section: indexPath.section) - 1 diff --git a/Stepic/Sources/Modules/WriteComment/InputOutput/WriteCommentOutputProtocol.swift b/Stepic/Sources/Modules/WriteComment/InputOutput/WriteCommentOutputProtocol.swift new file mode 100644 index 0000000000..a170fc888d --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/InputOutput/WriteCommentOutputProtocol.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol WriteCommentOutputProtocol: class { + func handleCommentCreated(_ comment: Comment) + func handleCommentUpdated(_ comment: Comment) +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentAssembly.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentAssembly.swift new file mode 100644 index 0000000000..6d4cbd3621 --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentAssembly.swift @@ -0,0 +1,41 @@ +import UIKit + +final class WriteCommentAssembly: Assembly { + private let targetID: WriteComment.TargetIDType + private let parentID: WriteComment.ParentIDtype? + private let presentationContext: WriteComment.PresentationContext + + private weak var moduleOutput: WriteCommentOutputProtocol? + + init( + targetID: WriteComment.TargetIDType, + parentID: WriteComment.ParentIDtype? = nil, + presentationContext: WriteComment.PresentationContext = .create, + output: WriteCommentOutputProtocol? = nil + ) { + self.targetID = targetID + self.parentID = parentID + self.presentationContext = presentationContext + self.moduleOutput = output + } + + func makeModule() -> UIViewController { + let provider = WriteCommentProvider( + commentsNetworkService: CommentsNetworkService(commentsAPI: CommentsAPI()) + ) + let presenter = WriteCommentPresenter() + let interactor = WriteCommentInteractor( + targetID: self.targetID, + parentID: self.parentID, + presentationContext: self.presentationContext, + presenter: presenter, + provider: provider + ) + let viewController = WriteCommentViewController(interactor: interactor) + + presenter.viewController = viewController + interactor.moduleOutput = self.moduleOutput + + return viewController + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentDataFlow.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentDataFlow.swift new file mode 100644 index 0000000000..ad6f32d1bd --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentDataFlow.swift @@ -0,0 +1,85 @@ +import Foundation + +enum WriteComment { + // MARK: Common types + + /// By backend architecture it's could be any object, but for now, only steps allowed. + /// `target` == `step_id`. + typealias TargetIDType = Step.IdType + typealias ParentIDtype = Comment.IdType + + enum PresentationContext { + case create + case edit(Comment) + } + + struct CommentInfo { + let text: String + let presentationContext: PresentationContext + } + + // MARK: - Use cases - + + /// Show comment + enum CommentLoad { + struct Request { } + + struct Response { + let data: CommentInfo + } + + struct ViewModel { + let state: ViewControllerState + } + } + + /// Handle review text change + enum CommentTextUpdate { + struct Request { + let text: String + } + + struct Response { + let data: CommentInfo + } + + struct ViewModel { + let state: ViewControllerState + } + } + + /// Do comment main action (create or update) + enum CommentMainAction { + struct Request { } + + struct Response { + let data: Result + } + + struct ViewModel { + let state: ViewControllerState + } + } + + /// Shows alert about changes losing + enum CommentCancelPresentation { + struct Request { } + + struct Response { + let originalText: String + let currentText: String + } + + struct ViewModel { + let shouldAskUser: Bool + } + } + + // MARK: States + + enum ViewControllerState { + case loading + case error + case result(data: WriteCommentViewModel) + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentInteractor.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentInteractor.swift new file mode 100644 index 0000000000..1c44ecba5d --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentInteractor.swift @@ -0,0 +1,111 @@ +import Foundation +import PromiseKit + +protocol WriteCommentInteractorProtocol { + func doCommentLoad(request: WriteComment.CommentLoad.Request) + func doCommentTextUpdate(request: WriteComment.CommentTextUpdate.Request) + func doCommentMainAction(request: WriteComment.CommentMainAction.Request) + func doCommentCancelPresentation(request: WriteComment.CommentCancelPresentation.Request) +} + +final class WriteCommentInteractor: WriteCommentInteractorProtocol { + weak var moduleOutput: WriteCommentOutputProtocol? + + private let targetID: WriteComment.TargetIDType + private let parentID: WriteComment.ParentIDtype? + private let presentationContext: WriteComment.PresentationContext + + private let presenter: WriteCommentPresenterProtocol + private let provider: WriteCommentProviderProtocol + + private let originalText: String + private var currentText: String + + init( + targetID: WriteComment.TargetIDType, + parentID: WriteComment.ParentIDtype?, + presentationContext: WriteComment.PresentationContext, + presenter: WriteCommentPresenterProtocol, + provider: WriteCommentProviderProtocol + ) { + self.targetID = targetID + self.parentID = parentID + self.presentationContext = presentationContext + self.presenter = presenter + self.provider = provider + + switch presentationContext { + case .create: + self.originalText = "" + case .edit(let comment): + self.originalText = comment.text + } + self.currentText = self.originalText + } + + func doCommentLoad(request: WriteComment.CommentLoad.Request) { + self.presenter.presentComment( + response: WriteComment.CommentLoad.Response(data: self.makeCommentInfo()) + ) + } + + func doCommentTextUpdate(request: WriteComment.CommentTextUpdate.Request) { + self.currentText = request.text + + self.presenter.presentCommentTextUpdate( + response: WriteComment.CommentTextUpdate.Response(data: self.makeCommentInfo()) + ) + } + + func doCommentMainAction(request: WriteComment.CommentMainAction.Request) { + let htmlText = self.currentText + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "\n", with: "
") + let currentComment = Comment(parent: self.parentID, target: self.targetID, text: htmlText) + + var actionPromise: Promise + var moduleOutputHandler: ((Comment) -> Void)? + + switch self.presentationContext { + case .create: + actionPromise = self.provider.create(comment: currentComment) + moduleOutputHandler = self.moduleOutput?.handleCommentCreated(_:) + case .edit(let comment): + currentComment.id = comment.id + actionPromise = self.provider.update(comment: currentComment) + moduleOutputHandler = self.moduleOutput?.handleCommentUpdated(_:) + } + + actionPromise.done { comment in + self.currentText = comment.text.replacingOccurrences(of: "
", with: "\n") + self.presenter.presentCommentMainActionResult( + response: WriteComment.CommentMainAction.Response( + data: .success(self.makeCommentInfo()) + ) + ) + moduleOutputHandler?(comment) + }.catch { error in + self.presenter.presentCommentMainActionResult( + response: WriteComment.CommentMainAction.Response(data: .failure(error)) + ) + } + } + + func doCommentCancelPresentation(request: WriteComment.CommentCancelPresentation.Request) { + self.presenter.presentCommentCancelPresentation( + response: WriteComment.CommentCancelPresentation.Response( + originalText: self.originalText, + currentText: self.currentText + ) + ) + } + + // MARK: - Private API + + private func makeCommentInfo() -> WriteComment.CommentInfo { + return WriteComment.CommentInfo( + text: self.currentText, + presentationContext: self.presentationContext + ) + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentPresenter.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentPresenter.swift new file mode 100644 index 0000000000..1fecfbf5ff --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentPresenter.swift @@ -0,0 +1,79 @@ +import UIKit + +protocol WriteCommentPresenterProtocol { + func presentComment(response: WriteComment.CommentLoad.Response) + func presentCommentTextUpdate(response: WriteComment.CommentTextUpdate.Response) + func presentCommentMainActionResult(response: WriteComment.CommentMainAction.Response) + func presentCommentCancelPresentation(response: WriteComment.CommentCancelPresentation.Response) +} + +final class WriteCommentPresenter: WriteCommentPresenterProtocol { + weak var viewController: WriteCommentViewControllerProtocol? + + func presentComment(response: WriteComment.CommentLoad.Response) { + let viewModel = self.makeViewModel( + text: response.data.text, + presentationContext: response.data.presentationContext + ) + self.viewController?.displayComment( + viewModel: WriteComment.CommentLoad.ViewModel(state: .result(data: viewModel)) + ) + } + + func presentCommentTextUpdate(response: WriteComment.CommentTextUpdate.Response) { + let viewModel = self.makeViewModel( + text: response.data.text, + presentationContext: response.data.presentationContext + ) + self.viewController?.displayCommentTextUpdate( + viewModel: WriteComment.CommentTextUpdate.ViewModel(state: .result(data: viewModel)) + ) + } + + func presentCommentMainActionResult(response: WriteComment.CommentMainAction.Response) { + switch response.data { + case .success(let data): + let viewModel = self.makeViewModel( + text: data.text, + presentationContext: data.presentationContext + ) + self.viewController?.displayCommentMainActionResult( + viewModel: WriteComment.CommentMainAction.ViewModel(state: .result(data: viewModel)) + ) + case .failure: + self.viewController?.displayCommentMainActionResult( + viewModel: WriteComment.CommentMainAction.ViewModel(state: .error) + ) + } + } + + func presentCommentCancelPresentation(response: WriteComment.CommentCancelPresentation.Response) { + self.viewController?.displayCommentCancelPresentation( + viewModel: WriteComment.CommentCancelPresentation.ViewModel( + shouldAskUser: response.originalText != response.currentText && !response.currentText.isEmpty + ) + ) + } + + // MARK: - Private API + + private func makeViewModel( + text: String, + presentationContext: WriteComment.PresentationContext + ) -> WriteCommentViewModel { + var buttonTitle: String + + switch presentationContext { + case .create: + buttonTitle = NSLocalizedString("WriteCommentActionButtonCreate", comment: "") + case .edit: + buttonTitle = NSLocalizedString("WriteCommentActionButtonEdit", comment: "") + } + + return WriteCommentViewModel( + text: text, + doneButtonTitle: buttonTitle, + isFilled: !text.isEmpty + ) + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentProvider.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentProvider.swift new file mode 100644 index 0000000000..41326fdf76 --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentProvider.swift @@ -0,0 +1,40 @@ +import Foundation +import PromiseKit + +protocol WriteCommentProviderProtocol { + func create(comment: Comment) -> Promise + func update(comment: Comment) -> Promise +} + +final class WriteCommentProvider: WriteCommentProviderProtocol { + private let commentsNetworkService: CommentsNetworkServiceProtocol + + init(commentsNetworkService: CommentsNetworkServiceProtocol) { + self.commentsNetworkService = commentsNetworkService + } + + func create(comment: Comment) -> Promise { + return Promise { seal in + self.commentsNetworkService.create(comment: comment).done { comment in + seal.fulfill(comment) + }.catch { _ in + seal.reject(Error.networkCreateFailed) + } + } + } + + func update(comment: Comment) -> Promise { + return Promise { seal in + self.commentsNetworkService.update(comment: comment).done { comment in + seal.fulfill(comment) + }.catch { _ in + seal.reject(Error.networkUpdateFailed) + } + } + } + + enum Error: Swift.Error { + case networkCreateFailed + case networkUpdateFailed + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentView.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentView.swift new file mode 100644 index 0000000000..62e49ad489 --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentView.swift @@ -0,0 +1,98 @@ +import SnapKit +import UIKit + +protocol WriteCommentViewDelegate: class { + func writeCommentView(_ view: WriteCommentView, didUpdateText text: String) +} + +extension WriteCommentView { + struct Appearance { + let backgroundColor = UIColor.white + + let textViewInsets = LayoutInsets(top: 16, left: 16, bottom: 16, right: 16) + let textViewFont = UIFont.systemFont(ofSize: 16) + let textViewTextColor = UIColor.mainDark + let textViewPlaceholderColor = UIColor.mainDark.withAlphaComponent(0.4) + } +} + +final class WriteCommentView: UIView { + let appearance: Appearance + + weak var delegate: WriteCommentViewDelegate? + + private lazy var textView: TableInputTextView = { + let textView = TableInputTextView() + textView.font = self.appearance.textViewFont + textView.textColor = self.appearance.textViewTextColor + textView.placeholderColor = self.appearance.textViewPlaceholderColor + textView.placeholder = NSLocalizedString("WriteCommentPlaceholder", comment: "") + textView.textInsets = .zero + + // Disable features + textView.dataDetectorTypes = [] + + textView.delegate = self + + return textView + }() + + var isEnabled: Bool = true { + didSet { + self.textView.isEditable = self.isEnabled + } + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func becomeFirstResponder() -> Bool { + return self.textView.becomeFirstResponder() + } + + func configure(viewModel: WriteCommentViewModel) { + self.textView.text = viewModel.text + } +} + +extension WriteCommentView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = self.appearance.backgroundColor + } + + func addSubviews() { + self.addSubview(self.textView) + } + + func makeConstraints() { + self.textView.translatesAutoresizingMaskIntoConstraints = false + self.textView.snp.makeConstraints { make in + make.leading.equalTo(self.safeAreaLayoutGuide.snp.leading).offset(self.appearance.textViewInsets.left) + make.top.equalToSuperview().offset(self.appearance.textViewInsets.top) + make.trailing.equalTo(self.safeAreaLayoutGuide.snp.trailing).offset(-self.appearance.textViewInsets.right) + make.bottom.equalTo(self.safeAreaLayoutGuide.snp.bottom).offset(-self.appearance.textViewInsets.bottom) + } + } +} + +// MARK: - WriteCommentView: UITextViewDelegate - + +extension WriteCommentView: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + self.delegate?.writeCommentView(self, didUpdateText: textView.text) + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentViewController.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentViewController.swift new file mode 100644 index 0000000000..8c36938e34 --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentViewController.swift @@ -0,0 +1,175 @@ +import IQKeyboardManagerSwift +import UIKit + +protocol WriteCommentViewControllerProtocol: class { + func displayComment(viewModel: WriteComment.CommentLoad.ViewModel) + func displayCommentTextUpdate(viewModel: WriteComment.CommentTextUpdate.ViewModel) + func displayCommentMainActionResult(viewModel: WriteComment.CommentMainAction.ViewModel) + func displayCommentCancelPresentation(viewModel: WriteComment.CommentCancelPresentation.ViewModel) +} + +final class WriteCommentViewController: UIViewController { + lazy var writeCommentView = self.view as? WriteCommentView + + private let interactor: WriteCommentInteractorProtocol + private var state: WriteComment.ViewControllerState + + private lazy var cancelBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(self.cancelButtonDidClick(_:)) + ) + + private lazy var doneBarButtonItem = UIBarButtonItem( + title: nil, + style: .done, + target: self, + action: #selector(self.doneButtonDidClick(_:)) + ) + + private lazy var activityBarButtonItem: UIBarButtonItem = { + let activityIndicatorView = UIActivityIndicatorView(style: .white) + activityIndicatorView.color = .mainDark + activityIndicatorView.startAnimating() + return UIBarButtonItem(customView: activityIndicatorView) + }() + + init( + interactor: WriteCommentInteractorProtocol, + initialState: WriteComment.ViewControllerState = .loading + ) { + self.interactor = interactor + self.state = initialState + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let view = WriteCommentView(frame: UIScreen.main.bounds) + view.delegate = self + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.title = NSLocalizedString("WriteCommentTitle", comment: "") + self.edgesForExtendedLayout = [] + + self.navigationItem.leftBarButtonItem = self.cancelBarButtonItem + self.navigationItem.rightBarButtonItem = self.doneBarButtonItem + + self.doneBarButtonItem.isEnabled = false + + self.updateState(newState: self.state) + self.interactor.doCommentLoad(request: .init()) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + IQKeyboardManager.shared.enable = false + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + _ = self.writeCommentView?.becomeFirstResponder() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.view.endEditing(true) + IQKeyboardManager.shared.enable = true + } + + // MARK: - Private API + + private func updateState(newState: WriteComment.ViewControllerState) { + switch newState { + case .result(let data): + self.writeCommentView?.isEnabled = true + self.writeCommentView?.configure(viewModel: data) + self.navigationItem.rightBarButtonItem = self.doneBarButtonItem + self.doneBarButtonItem.title = data.doneButtonTitle + self.doneBarButtonItem.isEnabled = data.isFilled + case .loading: + self.view.endEditing(true) + self.writeCommentView?.isEnabled = false + self.navigationItem.rightBarButtonItem = self.activityBarButtonItem + case .error: + self.writeCommentView?.isEnabled = true + _ = self.writeCommentView?.becomeFirstResponder() + self.navigationItem.rightBarButtonItem = self.doneBarButtonItem + self.doneBarButtonItem.isEnabled = true + } + self.state = newState + } + + @objc + private func cancelButtonDidClick(_ sender: UIBarButtonItem) { + self.interactor.doCommentCancelPresentation(request: .init()) + } + + @objc + private func doneButtonDidClick(_ sender: UIBarButtonItem) { + self.updateState(newState: .loading) + self.interactor.doCommentMainAction(request: .init()) + } +} + +// MARK: - WriteCommentViewController: WriteCommentViewControllerProtocol - + +extension WriteCommentViewController: WriteCommentViewControllerProtocol { + func displayComment(viewModel: WriteComment.CommentLoad.ViewModel) { + self.updateState(newState: viewModel.state) + } + + func displayCommentTextUpdate(viewModel: WriteComment.CommentTextUpdate.ViewModel) { + self.updateState(newState: viewModel.state) + } + + func displayCommentMainActionResult(viewModel: WriteComment.CommentMainAction.ViewModel) { + if case .result = viewModel.state { + self.dismiss(animated: true) + } else { + self.updateState(newState: viewModel.state) + } + } + + func displayCommentCancelPresentation(viewModel: WriteComment.CommentCancelPresentation.ViewModel) { + if viewModel.shouldAskUser { + let alert = UIAlertController( + title: nil, + message: NSLocalizedString("WriteCommentCancelPromptMessage", comment: ""), + preferredStyle: .alert + ) + + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) + let discardAction = UIAlertAction( + title: NSLocalizedString("WriteCommentCancelPromptDestructiveActionTitle", comment: ""), + style: .destructive, + handler: { [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + ) + + alert.addAction(cancelAction) + alert.addAction(discardAction) + + self.present(alert, animated: true) + } else { + self.dismiss(animated: true, completion: nil) + } + } +} + +// MARK: - WriteCommentViewController: WriteCommentViewDelegate - + +extension WriteCommentViewController: WriteCommentViewDelegate { + func writeCommentView(_ view: WriteCommentView, didUpdateText text: String) { + self.interactor.doCommentTextUpdate(request: .init(text: text)) + } +} diff --git a/Stepic/Sources/Modules/WriteComment/WriteCommentViewModel.swift b/Stepic/Sources/Modules/WriteComment/WriteCommentViewModel.swift new file mode 100644 index 0000000000..248cf3ab73 --- /dev/null +++ b/Stepic/Sources/Modules/WriteComment/WriteCommentViewModel.swift @@ -0,0 +1,7 @@ +import Foundation + +struct WriteCommentViewModel { + let text: String + let doneButtonTitle: String + let isFilled: Bool +} diff --git a/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift b/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift index 5599f3cb57..732ae9ab65 100644 --- a/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift +++ b/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift @@ -4,6 +4,8 @@ import PromiseKit protocol CommentsNetworkServiceProtocol: class { func fetch(ids: [Comment.IdType]) -> Promise<[Comment]> func create(comment: Comment) -> Promise + func update(comment: Comment) -> Promise + func delete(id: Comment.IdType) -> Promise } final class CommentsNetworkService: CommentsNetworkServiceProtocol { @@ -33,12 +35,35 @@ final class CommentsNetworkService: CommentsNetworkServiceProtocol { self.commentsAPI.create(comment).done { comment in seal.fulfill(comment) }.catch { _ in - seal.reject(Error.fetchFailed) + seal.reject(Error.createFailed) + } + } + } + + func update(comment: Comment) -> Promise { + return Promise { seal in + self.commentsAPI.update(comment).done { comment in + seal.fulfill(comment) + }.catch { _ in + seal.reject(Error.updateFailed) + } + } + } + + func delete(id: Comment.IdType) -> Promise { + return Promise { seal in + self.commentsAPI.delete(commentID: id).done { + seal.fulfill(()) + }.catch { _ in + seal.reject(Error.deleteFailed) } } } enum Error: Swift.Error { case fetchFailed + case createFailed + case updateFailed + case deleteFailed } } diff --git a/Stepic/SubmissionsAPI.swift b/Stepic/SubmissionsAPI.swift index ad5397c92a..90a4775787 100644 --- a/Stepic/SubmissionsAPI.swift +++ b/Stepic/SubmissionsAPI.swift @@ -137,12 +137,12 @@ final class SubmissionsAPI: APIEndpoint { func create(stepName: String, attemptId: Int, reply: Reply) -> Promise { let submission = Submission(attempt: attemptId, reply: reply) return Promise { seal in - create.request(requestEndpoint: "submissions", paramName: "submission", creatingObject: submission, withManager: manager).done { - submission, json in - guard let json = json else { - seal.fulfill(submission) - return - } + self.create.request( + requestEndpoint: "submissions", + paramName: "submission", + creatingObject: submission, + withManager: manager + ).done { submission, json in submission.initReply(json: json["submissions"].arrayValue[0]["reply"], stepName: stepName) seal.fulfill(submission) }.catch { diff --git a/Stepic/UpdateRequestMaker.swift b/Stepic/UpdateRequestMaker.swift index ef68625f83..de405f6975 100644 --- a/Stepic/UpdateRequestMaker.swift +++ b/Stepic/UpdateRequestMaker.swift @@ -9,26 +9,56 @@ import Alamofire import Foundation import PromiseKit +import SwiftyJSON final class UpdateRequestMaker { - func request(requestEndpoint: String, paramName: String, updatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise { + func request( + requestEndpoint: String, + paramName: String, + updatingObject: T, + withManager manager: Alamofire.SessionManager + ) -> Promise<(T, JSON)> { return Promise { seal in let params: Parameters? = [ paramName: updatingObject.json.dictionaryObject ?? "" ] checkToken().done { - manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(updatingObject.id)", method: .put, parameters: params, encoding: JSONEncoding.default).validate().responseSwiftyJSON { response in + manager.request( + "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(updatingObject.id)", + method: .put, + parameters: params, + encoding: JSONEncoding.default + ).validate().responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(NetworkError(error: error)) case .success(let json): updatingObject.update(json: json[requestEndpoint].arrayValue[0]) - seal.fulfill(updatingObject) + seal.fulfill((updatingObject, json)) } } - }.catch { - error in + }.catch { error in + seal.reject(error) + } + } + } + + func request( + requestEndpoint: String, + paramName: String, + updatingObject: T, + withManager manager: Alamofire.SessionManager + ) -> Promise { + return Promise { seal in + self.request( + requestEndpoint: requestEndpoint, + paramName: paramName, + updatingObject: updatingObject, + withManager: manager + ).done { updatedObject, _ in + seal.fulfill(updatedObject) + }.catch { error in seal.reject(error) } } diff --git a/Stepic/WriteCommentViewController.swift b/Stepic/WriteCommentViewController.swift deleted file mode 100644 index b3ca0da1bc..0000000000 --- a/Stepic/WriteCommentViewController.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// WriteCommentViewController.swift -// Stepic -// -// Created by Alexander Karpov on 14.06.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import IQKeyboardManagerSwift -import UIKit - -@available(*, deprecated, message: "Legacy assembly") -final class WriteCommentLegacyAssembly: Assembly { - private let target: Step.IdType - private let parentId: Comment.IdType? - - private weak var delegate: WriteCommentViewControllerDelegate? - - init(target: Int, parentId: Comment.IdType?, delegate: WriteCommentViewControllerDelegate? = nil) { - self.target = target - self.parentId = parentId - self.delegate = delegate - } - - func makeModule() -> UIViewController { - guard let vc = ControllerHelper.instantiateViewController( - identifier: "WriteCommentViewController", - storyboardName: "DiscussionsStoryboard" - ) as? WriteCommentViewController else { - fatalError() - } - - vc.parentId = self.parentId - vc.target = self.target - vc.delegate = self.delegate - - return vc - } -} - -protocol WriteCommentViewControllerDelegate: class { - func writeCommentViewControllerDidWriteComment(_ controller: WriteCommentViewController, comment: Comment) -} - -final class WriteCommentViewController: UIViewController { - enum State { - case editing - case sending - case ok - } - - @IBOutlet weak var commentTextView: IQTextView! - - weak var delegate: WriteCommentViewControllerDelegate? - - var target: Int! - var parentId: Int? - - private let commentsAPI = CommentsAPI() - - private lazy var editBarButtonItem: UIBarButtonItem = { - UIBarButtonItem( - image: Images.sendImage, - style: .done, - target: self, - action: #selector(WriteCommentViewController.sendPressed) - ) - }() - - private lazy var sendBarButtonItem: UIBarButtonItem = { - let activityIndicatorView = UIActivityIndicatorView() - activityIndicatorView.color = .mainDark - activityIndicatorView.startAnimating() - return UIBarButtonItem(customView: activityIndicatorView) - }() - - private lazy var okBarButtonItem: UIBarButtonItem = { - UIBarButtonItem( - image: Images.checkMarkImage, - style: .done, - target: self, - action: #selector(WriteCommentViewController.okPressed) - ) - }() - - private var state: State = .editing { - didSet { - switch self.state { - case .sending: - self.view.endEditing(true) - self.navigationItem.rightBarButtonItem = self.sendBarButtonItem - case .ok: - self.navigationItem.rightBarButtonItem = self.okBarButtonItem - case .editing: - self.commentTextView.becomeFirstResponder() - self.navigationItem.rightBarButtonItem = self.editBarButtonItem - } - } - } - - private var htmlText: String { - let text = self.commentTextView.text ?? "" - return text.replacingOccurrences(of: "\n", with: "
") - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.title = NSLocalizedString("Comment", comment: "") - - self.commentTextView.placeholder = NSLocalizedString("WriteComment", comment: "") - self.commentTextView.tintColor = .mainDark - self.commentTextView.textColor = .mainText - - self.state = .editing - } - - @objc - private func okPressed() { - print("should have never been pressed") - } - - @objc - private func sendPressed() { - self.state = .sending - let comment = Comment(parent: self.parentId, target: self.target, text: self.htmlText) - - self.commentsAPI.create(comment).done { [weak self] comment in - guard let strongSelf = self else { - return - } - - strongSelf.state = .ok - - strongSelf.delegate?.writeCommentViewControllerDidWriteComment(strongSelf, comment: comment) - strongSelf.navigationController?.popViewController(animated: true) - }.catch { [weak self] _ in - self?.state = .editing - } - } -} diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 8fa19d6175..c38782ccfe 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -116,16 +116,10 @@ NewCommentAlertMessage = "Show comment in web?"; Reply = "Reply"; ShowComments = "Show discussions"; Discussions = "Discussions"; -WriteComment = "Write comment..."; -Comment = "Comment"; ShowMoreReplies = "More replies"; ShowMoreDiscussions = "More discussions"; RefreshingDiscussions = "Refreshing discussions..."; NoDiscussionsTitle = "No discussions. Start the first one!"; -Like = "Like"; -Unlike = "Unlike"; -Abuse = "Abuse"; -Unabuse = "Don't abuse"; Syllabus = "Syllabus"; Course = "Course"; NextLesson = "Next lesson"; @@ -657,12 +651,6 @@ WriteCourseReviewPlaceholder = "Review"; WriteCourseReviewRatingHint = "Tap a Star to Rate"; WriteCourseReviewActionNotAllowedDescription = "To write a review, complete more than 80% steps"; -/* Discussions */ -DiscussionsSortTypeLastDiscussions = "Last discussions"; -DiscussionsSortTypeMostLikedDiscussions = "Most liked"; -DiscussionsSortTypeMostActiveDiscussions = "Most active"; -DiscussionsSortTypeRecentActivityDiscussions = "Recent activity"; - /* ExamEGERussian */ "ErrorMessage" = "Sorry, something went wrong: please try again later."; "Ok" = "Ok"; @@ -774,3 +762,25 @@ CodeQuizEmptyCodeLanguages = "No supported languages"; CodeQuizFullscreenTabInstructionTitle = "Instruction"; CodeQuizFullscreenTabCodeTitle = "Code"; CodeQuizFullscreenTabRunTitle = "Run"; + +/* Discussions */ +DiscussionsTitle = "Discussions"; +DiscussionsAlertActionEditTitle = "Edit"; +DiscussionsAlertActionDeleteTitle = "Delete"; +DiscussionsAlertActionLikeTitle = "Like"; +DiscussionsAlertActionUnlikeTitle = "Unlike"; +DiscussionsAlertActionAbuseTitle = "Abuse"; +DiscussionsAlertActionUnabuseTitle = "Don't abuse"; +DiscussionsSortTypeAlertTitle = "Sort by"; +DiscussionsSortTypeLastDiscussions = "Last discussions"; +DiscussionsSortTypeMostLikedDiscussions = "Most liked"; +DiscussionsSortTypeMostActiveDiscussions = "Most active"; +DiscussionsSortTypeRecentActivityDiscussions = "Recent activity"; + +/* Write comment */ +WriteCommentTitle = "Comment"; +WriteCommentPlaceholder = "Leave a comment..."; +WriteCommentActionButtonCreate = "Send"; +WriteCommentActionButtonEdit = "Update"; +WriteCommentCancelPromptMessage = "Delete draft?"; +WriteCommentCancelPromptDestructiveActionTitle = "Delete"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 7361da8b5c..0359cd2034 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -116,16 +116,10 @@ NewCommentAlertMessage = "Прочитать комментарий в брау Reply = "Ответить"; ShowComments = "Показать комментарии"; Discussions = "Комментарии"; -WriteComment = "Написать комментарий..."; -Comment = "Комментарий"; ShowMoreReplies = "Больше ответов"; ShowMoreDiscussions = "Больше комментариев"; RefreshingDiscussions = "Обновляем комментарии..."; NoDiscussionsTitle = "Обсуждений нет. Начните первое!"; -Like = "Нравится"; -Unlike = "Больше не нравится"; -Abuse = "Пожаловаться"; -Unabuse = "Не жаловаться"; Syllabus = "Модули"; Course = "Курс"; NextLesson = "Следующий урок"; @@ -658,12 +652,6 @@ WriteCourseReviewPlaceholder = "Отзыв"; WriteCourseReviewRatingHint = "Коснитесь для оценки"; WriteCourseReviewActionNotAllowedDescription = "Чтобы оставить отзыв, пройдите больше 80% материалов"; -/* Discussions */ -DiscussionsSortTypeLastDiscussions = "Новые обсуждения"; -DiscussionsSortTypeMostLikedDiscussions = "Самые популярные"; -DiscussionsSortTypeMostActiveDiscussions = "Самые обсуждаемые"; -DiscussionsSortTypeRecentActivityDiscussions = "Свежие обновления"; - /* ExamEGERussian */ "ErrorMessage" = "Извините, что-то пошло не так: повторите попытку позже."; "Ok" = "Ok"; @@ -775,3 +763,25 @@ CodeQuizEmptyCodeLanguages = "Нет поддерживаемых языков CodeQuizFullscreenTabInstructionTitle = "Инструкция"; CodeQuizFullscreenTabCodeTitle = "Код"; CodeQuizFullscreenTabRunTitle = "Запуск"; + +/* Discussions */ +DiscussionsTitle = "Комментарии"; +DiscussionsAlertActionEditTitle = "Редактировать"; +DiscussionsAlertActionDeleteTitle = "Удалить"; +DiscussionsAlertActionLikeTitle = "Нравится"; +DiscussionsAlertActionUnlikeTitle = "Больше не нравится"; +DiscussionsAlertActionAbuseTitle = "Пожаловаться"; +DiscussionsAlertActionUnabuseTitle = "Не жаловаться"; +DiscussionsSortTypeAlertTitle = "Сортировать по"; +DiscussionsSortTypeLastDiscussions = "Новые обсуждения"; +DiscussionsSortTypeMostLikedDiscussions = "Самые популярные"; +DiscussionsSortTypeMostActiveDiscussions = "Самые обсуждаемые"; +DiscussionsSortTypeRecentActivityDiscussions = "Свежие обновления"; + +/* Write comment */ +WriteCommentTitle = "Комментарий"; +WriteCommentPlaceholder = "Оставьте комментарий..."; +WriteCommentActionButtonCreate = "Отправить"; +WriteCommentActionButtonEdit = "Обновить"; +WriteCommentCancelPromptMessage = "Удалить черновик?"; +WriteCommentCancelPromptDestructiveActionTitle = "Удалить"; diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index 5eb42c3ece..d587f7ea11 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.98 + 1.99 CFBundleSignature ???? CFBundleVersion - 156 + 157
diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index 86c81d75f4..8ffe03465d 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.98 + 1.99 CFBundleVersion - 156 + 157 NSExtension NSExtensionPointIdentifier