diff --git a/Cartfile b/Cartfile index d9534782..c64e48ab 100755 --- a/Cartfile +++ b/Cartfile @@ -22,3 +22,4 @@ github "jrendel/SwiftKeychainWrapper" ~> 3.4 github "cbpowell/MarqueeLabel" "4.0.5" github "scinfu/SwiftSoup" ~> 2.3.2 github "Instagram/IGListKit" ~> 4.0.0 +github "ra1028/Former" diff --git a/Cartfile.resolved b/Cartfile.resolved index d18ea417..87fc6066 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -5,6 +5,7 @@ github "Alua-Kinzhebayeva/iOS-PDF-Reader" "2.5.1" github "AssistoLab/DropDown" "v2.3.1" github "AtomicSLLC/SlideMenuControllerSwift" "5.0.0" github "Hearst-DD/ObjectMapper" "3.4.0" +github "Instagram/IGListKit" "4.0.0" github "JanGorman/Hippolyte" "0.6.0" github "LaurentiuUngur/LUExpandableTableView" "3.0.0" github "M3U8Kit/M3U8Parser" "0.2.6" @@ -17,8 +18,9 @@ github "getsentry/sentry-cocoa" "4.3.1" github "hackiftekhar/IQKeyboardManager" "v5.0.6" github "jrendel/SwiftKeychainWrapper" "3.4.0" github "onevcat/Kingfisher" "4.10.0" +github "ra1028/Former" "1.8.1" github "realm/realm-cocoa" "v3.20.0" github "schickling/Device.swift" "1.1.2" -github "scinfu/SwiftSoup" "2.3.2" +github "scinfu/SwiftSoup" "2.3.3" github "xmartlabs/XLPagerTabStrip" "8.1.0" github "zekunyan/TTGSnackbar" "1.7.1" diff --git a/ios-app.xcodeproj/project.pbxproj b/ios-app.xcodeproj/project.pbxproj index 254881da..4715e7e5 100644 --- a/ios-app.xcodeproj/project.pbxproj +++ b/ios-app.xcodeproj/project.pbxproj @@ -44,6 +44,11 @@ 2548C4922476CFAC00F90D09 /* BaseDBTableViewControllerV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2548C4912476CFAC00F90D09 /* BaseDBTableViewControllerV2.swift */; }; 254D8F1225813D90001A54FE /* MobileRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 254D8F1125813D8F001A54FE /* MobileRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 254E69C421DCF199009A4E61 /* Testpress_iOS_AppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 254E69C321DCF199009A4E61 /* Testpress_iOS_AppUITests.swift */; }; + 25520BB62750FAC0001A58AB /* ForumFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25520BB52750FAC0001A58AB /* ForumFilterViewController.swift */; }; + 25520BB92750FB4F001A58AB /* Former.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25520BB82750FB4E001A58AB /* Former.framework */; }; + 25520BBE2751093D001A58AB /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25520BBD2751093C001A58AB /* String.swift */; }; + 25520BC327546AF9001A58AB /* DiscussionThreadDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25520BC227546AF9001A58AB /* DiscussionThreadDetailViewController.swift */; }; + 25520BC627547006001A58AB /* DiscussionThreadAnswer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25520BC527547006001A58AB /* DiscussionThreadAnswer.swift */; }; 2555A0C42466A59400D56707 /* ContentsListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2555A0C32466A59400D56707 /* ContentsListResponse.swift */; }; 25681BD0252F1D69002352E8 /* MobileRTCResources.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 25681BCF252F1D68002352E8 /* MobileRTCResources.bundle */; }; 25681BD1252F1DB6002352E8 /* MobileRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25681BCD252F1D4A002352E8 /* MobileRTC.framework */; }; @@ -67,6 +72,7 @@ 257BB695246AB4C900524D2C /* QuizExamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 257BB694246AB4C900524D2C /* QuizExamViewController.swift */; }; 257BB697246AC22B00524D2C /* ExamQuestionsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 257BB696246AC22B00524D2C /* ExamQuestionsResponse.swift */; }; 257BB699246AC2ED00524D2C /* ExamQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 257BB698246AC2ED00524D2C /* ExamQuestion.swift */; }; + 258EDBA1275A13B300042311 /* ReportDiscussionThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 258EDBA0275A13B300042311 /* ReportDiscussionThread.swift */; }; 2590B1FA2510BB90003C0A03 /* VideoConference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2590B1F92510BB90003C0A03 /* VideoConference.swift */; }; 2590B1FC2510C9F2003C0A03 /* VideoConferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2590B1FB2510C9F2003C0A03 /* VideoConferenceViewController.swift */; }; 2592868A2473B1E00035EBA4 /* QuizReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259286892473B1E00035EBA4 /* QuizReviewViewController.swift */; }; @@ -75,6 +81,7 @@ 25985835248A77BC003591FA /* EncryptionKeyRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25985834248A77BC003591FA /* EncryptionKeyRepository.swift */; }; 25985837248A77F6003591FA /* M3U8Handler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25985836248A77F6003591FA /* M3U8Handler.swift */; }; 259E2FC92428991B0096B6D1 /* ShareToUnlockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E2FC82428991B0096B6D1 /* ShareToUnlockViewController.swift */; }; + 259ED9F9274E5B3100A87A71 /* Misc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259ED9F8274E5B3100A87A71 /* Misc.swift */; }; 25AA1B2E252B2274008AAA9D /* TestVideoConferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25AA1B2D252B2274008AAA9D /* TestVideoConferenceViewController.swift */; }; 25B0ADC621D4D48800C702B9 /* VerifyPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25B0ADC521D4D48800C702B9 /* VerifyPhoneViewController.swift */; }; 25B0ADC821D5A29600C702B9 /* PhoneVerification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25B0ADC721D5A29500C702B9 /* PhoneVerification.swift */; }; @@ -428,6 +435,7 @@ 250E68F521BA4AA600DA81F1 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 2510859A2489E1EE00CDC2B5 /* VideoPlayerResourceLoaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerResourceLoaderDelegate.swift; sourceTree = ""; }; 2519C12D21B0083600D26551 /* InstituteSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstituteSettings.swift; sourceTree = ""; }; + 25220C84274F8F6400609A8E /* QuickTableViewController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickTableViewController.framework; path = Carthage/Build/iOS/QuickTableViewController.framework; sourceTree = ""; }; 252E050D25C2C06800BE1B38 /* MarqueeLabel.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MarqueeLabel.framework; path = Carthage/Build/iOS/MarqueeLabel.framework; sourceTree = ""; }; 253DD67D262860B200C6D0A8 /* GapFillResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GapFillResponse.swift; sourceTree = ""; }; 253DD67F262863C900C6D0A8 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = Carthage/Build/iOS/SwiftSoup.framework; sourceTree = ""; }; @@ -436,6 +444,12 @@ 254E69C121DCF199009A4E61 /* Testpress iOS AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Testpress iOS AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 254E69C321DCF199009A4E61 /* Testpress_iOS_AppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Testpress_iOS_AppUITests.swift; sourceTree = ""; }; 254E69C521DCF199009A4E61 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 25520BB22750F50A001A58AB /* AUPickerCell.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AUPickerCell.framework; path = Carthage/Build/iOS/AUPickerCell.framework; sourceTree = ""; }; + 25520BB52750FAC0001A58AB /* ForumFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForumFilterViewController.swift; sourceTree = ""; }; + 25520BB82750FB4E001A58AB /* Former.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Former.framework; path = Carthage/Build/iOS/Former.framework; sourceTree = ""; }; + 25520BBD2751093C001A58AB /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + 25520BC227546AF9001A58AB /* DiscussionThreadDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionThreadDetailViewController.swift; sourceTree = ""; }; + 25520BC527547006001A58AB /* DiscussionThreadAnswer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionThreadAnswer.swift; sourceTree = ""; }; 2555A0C32466A59400D56707 /* ContentsListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentsListResponse.swift; sourceTree = ""; }; 25681BCD252F1D4A002352E8 /* MobileRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileRTC.framework; path = "../Downloads/ios-mobilertc-all-5.0.24433.0616-clientlog/lib/MobileRTC.framework"; sourceTree = ""; }; 25681BCF252F1D68002352E8 /* MobileRTCResources.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = MobileRTCResources.bundle; path = "../Downloads/ios-mobilertc-all-5.0.24433.0616-clientlog/lib/MobileRTCResources.bundle"; sourceTree = ""; }; @@ -464,6 +478,7 @@ 257BB694246AB4C900524D2C /* QuizExamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizExamViewController.swift; sourceTree = ""; }; 257BB696246AC22B00524D2C /* ExamQuestionsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamQuestionsResponse.swift; sourceTree = ""; }; 257BB698246AC2ED00524D2C /* ExamQuestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamQuestion.swift; sourceTree = ""; }; + 258EDBA0275A13B300042311 /* ReportDiscussionThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportDiscussionThread.swift; sourceTree = ""; }; 2590B1F92510BB90003C0A03 /* VideoConference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoConference.swift; sourceTree = ""; }; 2590B1FB2510C9F2003C0A03 /* VideoConferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoConferenceViewController.swift; sourceTree = ""; }; 259286892473B1E00035EBA4 /* QuizReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizReviewViewController.swift; sourceTree = ""; }; @@ -472,6 +487,7 @@ 25985834248A77BC003591FA /* EncryptionKeyRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyRepository.swift; sourceTree = ""; }; 25985836248A77F6003591FA /* M3U8Handler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = M3U8Handler.swift; sourceTree = ""; }; 259E2FC82428991B0096B6D1 /* ShareToUnlockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToUnlockViewController.swift; sourceTree = ""; }; + 259ED9F8274E5B3100A87A71 /* Misc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Misc.swift; sourceTree = ""; }; 25AA1B2D252B2274008AAA9D /* TestVideoConferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestVideoConferenceViewController.swift; sourceTree = ""; }; 25B0ADC521D4D48800C702B9 /* VerifyPhoneViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyPhoneViewController.swift; sourceTree = ""; }; 25B0ADC721D5A29500C702B9 /* PhoneVerification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneVerification.swift; sourceTree = ""; }; @@ -767,6 +783,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 25520BB92750FB4F001A58AB /* Former.framework in Frameworks */, 25B60E93263FCBCD006CDDBE /* IGListDiffKit.framework in Frameworks */, 25B60E90263FCB95006CDDBE /* IGListKit.framework in Frameworks */, 253DD680262863C900C6D0A8 /* SwiftSoup.framework in Frameworks */, @@ -983,6 +1000,9 @@ 2FBEDB781E82BBD0000CF05C /* Frameworks */ = { isa = PBXGroup; children = ( + 25520BB82750FB4E001A58AB /* Former.framework */, + 25520BB22750F50A001A58AB /* AUPickerCell.framework */, + 25220C84274F8F6400609A8E /* QuickTableViewController.framework */, 25B60E92263FCBCD006CDDBE /* IGListDiffKit.framework */, 25B60E8F263FCB95006CDDBE /* IGListKit.framework */, 253DD67F262863C900C6D0A8 /* SwiftSoup.framework */, @@ -1222,6 +1242,9 @@ 2590B1FB2510C9F2003C0A03 /* VideoConferenceViewController.swift */, 250888D9251C5EC80023DCF6 /* ZoomMeetViewController.swift */, 25E435BA263AD8CC00243FD3 /* DashboardViewController.swift */, + 25520BB52750FAC0001A58AB /* ForumFilterViewController.swift */, + 25520BC227546AF9001A58AB /* DiscussionThreadDetailViewController.swift */, + 258EDBA0275A13B300042311 /* ReportDiscussionThread.swift */, ); path = UI; sourceTree = ""; @@ -1276,6 +1299,7 @@ 257BB698246AC2ED00524D2C /* ExamQuestion.swift */, 2590B1F92510BB90003C0A03 /* VideoConference.swift */, 253DD67D262860B200C6D0A8 /* GapFillResponse.swift */, + 25520BC527547006001A58AB /* DiscussionThreadAnswer.swift */, ); path = Model; sourceTree = ""; @@ -1354,6 +1378,8 @@ 256A909D247D21F500F4E12C /* UIButton.swift */, 256A90A1247D30DA00F4E12C /* Collection.swift */, 25E435D4263ADBAA00243FD3 /* UserDefaults.swift */, + 259ED9F8274E5B3100A87A71 /* Misc.swift */, + 25520BBD2751093C001A58AB /* String.swift */, ); path = Extensions; sourceTree = ""; @@ -1612,6 +1638,7 @@ "$(SRCROOT)/Carthage/Build/iOS/SwiftSoup.framework", "$(SRCROOT)/Carthage/Build/iOS/IGListKit.framework", "$(SRCROOT)/Carthage/Build/iOS/IGListDiffKit.framework", + "$(SRCROOT)/Carthage/Build/iOS/Former.framework", ); name = "Run Script"; outputPaths = ( @@ -1643,6 +1670,7 @@ 8C3D6431239D1D8C001C7FE4 /* VideoContentViewModel.swift in Sources */, 2FD846601FC30D1900D6F016 /* ContentDetailDataSource.swift in Sources */, 25E435E6263ADD9100243FD3 /* LeaderboardItemViewCell.swift in Sources */, + 25520BB62750FAC0001A58AB /* ForumFilterViewController.swift in Sources */, 25E435CF263ADA6C00243FD3 /* LeaderboardItem.swift in Sources */, 250888DA251C5EC80023DCF6 /* ZoomMeetViewController.swift in Sources */, 2F48C80B1FD288C6009C686C /* PostTableViewController.swift in Sources */, @@ -1661,6 +1689,7 @@ 2F412CC220219BC300B7F70C /* OrderedDictionary.swift in Sources */, 2F25D55C201EFE7B00C01CCE /* LeaderboardPager.swift in Sources */, 2FEDFF5A206CE7560002CF04 /* CategoryPager.swift in Sources */, + 25520BC327546AF9001A58AB /* DiscussionThreadDetailViewController.swift in Sources */, 2FC0F2E51F863AE00092BFDC /* TPAuthToken.swift in Sources */, 8C04228F22AA3FD40015269B /* WebViewController.swift in Sources */, 25E435DD263ADCCF00243FD3 /* BaseSectionController.swift in Sources */, @@ -1714,6 +1743,7 @@ 2FC0F2EF1F863B110092BFDC /* ExamPager.swift in Sources */, 2590B1FC2510C9F2003C0A03 /* VideoConferenceViewController.swift in Sources */, 2F5E0BBB1FE2871900236AFE /* ContentAttempt.swift in Sources */, + 25520BC627547006001A58AB /* DiscussionThreadAnswer.swift in Sources */, 25E435CC263ADA4300243FD3 /* Banner.swift in Sources */, 2F452C3020BBE3E6007833A0 /* BookmarksDetailDataSource.swift in Sources */, 2F4A81EF20E52CB50062DE8A /* BaseTableViewCell.swift in Sources */, @@ -1780,6 +1810,7 @@ 2FC0F3131F863BBD0092BFDC /* FormatDate.swift in Sources */, 250629942265E8E900539FB2 /* LoginActivityPager.swift in Sources */, 2503DF50263C01E8003226AD /* CarouselSectionController.swift in Sources */, + 25520BBE2751093D001A58AB /* String.swift in Sources */, 8CA1E880239A76AC0001FA5B /* UIView.swift in Sources */, 2FC0F2F01F863B130092BFDC /* AttemptPager.swift in Sources */, 2592868C2474316C0035EBA4 /* AttemptItemRepository.swift in Sources */, @@ -1793,6 +1824,7 @@ 2F763D0F1FBECBBC0033D495 /* Chapter.swift in Sources */, 2592868A2473B1E00035EBA4 /* QuizReviewViewController.swift in Sources */, 2F56B73D1FDE92CD00E2B240 /* TimeAnalyticsHeaderViewCell.swift in Sources */, + 259ED9F9274E5B3100A87A71 /* Misc.swift in Sources */, 8C2E7B602397B56600BA2F41 /* VideoPlayerControlsView.swift in Sources */, 2F17C1FE20C965B300606D5C /* BookmarkFoldersDropDown.swift in Sources */, 2FC0F2E41F863ADC0092BFDC /* TPApiResponse.swift in Sources */, @@ -1855,6 +1887,7 @@ 2F3DD22020187B5D004EDAF2 /* ApiResponse.swift in Sources */, 2570501B246EA32600E6C688 /* LoadingViewController.swift in Sources */, 2F3550901F9E12D2001964D6 /* BaseQuestionsSlidingViewController.swift in Sources */, + 258EDBA1275A13B300042311 /* ReportDiscussionThread.swift in Sources */, 2F3A53011FDA652A00C64123 /* Subject.swift in Sources */, 25985837248A77F6003591FA /* M3U8Handler.swift in Sources */, 2FA9285020B7DF2200CD3076 /* StringExtension.swift in Sources */, @@ -2134,7 +2167,7 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = "ios-app/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.20.0; OTHER_LDFLAGS = ( @@ -2161,7 +2194,7 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = "ios-app/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.20.0; OTHER_LDFLAGS = ( diff --git a/ios-app/Assets.xcassets/filter.imageset/Contents.json b/ios-app/Assets.xcassets/filter.imageset/Contents.json new file mode 100644 index 00000000..e4fa4c35 --- /dev/null +++ b/ios-app/Assets.xcassets/filter.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "filter.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-app/Assets.xcassets/filter.imageset/filter.png b/ios-app/Assets.xcassets/filter.imageset/filter.png new file mode 100644 index 00000000..4571d6db Binary files /dev/null and b/ios-app/Assets.xcassets/filter.imageset/filter.png differ diff --git a/ios-app/Base.lproj/Main.storyboard b/ios-app/Base.lproj/Main.storyboard index 5648186c..e1f44924 100644 --- a/ios-app/Base.lproj/Main.storyboard +++ b/ios-app/Base.lproj/Main.storyboard @@ -1734,8 +1734,29 @@ + + + + + + + + + + - + @@ -1763,10 +1784,13 @@ - + + + + @@ -1779,6 +1803,7 @@ + @@ -1790,9 +1815,13 @@ - + + + + + @@ -1924,7 +1953,7 @@ - + @@ -2140,7 +2169,7 @@ - + @@ -2205,7 +2234,7 @@ - + @@ -2964,6 +2993,7 @@ + @@ -2987,6 +3017,9 @@ + + + diff --git a/ios-app/Base.lproj/Post.storyboard b/ios-app/Base.lproj/Post.storyboard index 6ffe4ad1..b8e0b59c 100644 --- a/ios-app/Base.lproj/Post.storyboard +++ b/ios-app/Base.lproj/Post.storyboard @@ -1,9 +1,9 @@ - + - + @@ -95,6 +95,7 @@ + @@ -111,7 +112,6 @@ - @@ -161,7 +161,7 @@ - + @@ -171,7 +171,7 @@ - + @@ -187,7 +187,7 @@ - + @@ -206,7 +206,7 @@ - + - + - - + @@ -300,6 +300,7 @@ + @@ -311,7 +312,6 @@ - @@ -455,6 +455,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-app/Extensions/Misc.swift b/ios-app/Extensions/Misc.swift new file mode 100644 index 00000000..5bc4bb93 --- /dev/null +++ b/ios-app/Extensions/Misc.swift @@ -0,0 +1,10 @@ +import Foundation + +enum Debounce { + static func input(_ input: T, delay: TimeInterval = 0.3, current: @escaping @autoclosure () -> T, perform: @escaping (T) -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + guard input == current() else { return } + perform(input) + } + } +} diff --git a/ios-app/Extensions/String.swift b/ios-app/Extensions/String.swift new file mode 100644 index 00000000..234b7159 --- /dev/null +++ b/ios-app/Extensions/String.swift @@ -0,0 +1,11 @@ +import Foundation + +extension String { + static func mediumDateShortTime(date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.locale = .current + dateFormatter.timeStyle = .none + dateFormatter.dateStyle = .medium + return dateFormatter.string(from: date) + } +} diff --git a/ios-app/Model/DiscussionThreadAnswer.swift b/ios-app/Model/DiscussionThreadAnswer.swift new file mode 100644 index 00000000..d58096cd --- /dev/null +++ b/ios-app/Model/DiscussionThreadAnswer.swift @@ -0,0 +1,22 @@ + +import ObjectMapper + +class DiscussionThreadAnswer { + var id: Int! + var approvedBy: User! + var forumThreadId: Int! + var comment: Comment! + + + public required init?(map: Map) { + } +} + +extension DiscussionThreadAnswer: TestpressModel { + public func mapping(map: Map) { + id <- map["id"] + approvedBy <- map["approved_by"] + forumThreadId <- map["forum_thread_id"] + comment <- map["comment"] + } +} diff --git a/ios-app/Model/Post.swift b/ios-app/Model/Post.swift index af6ac211..a7461a60 100644 --- a/ios-app/Model/Post.swift +++ b/ios-app/Model/Post.swift @@ -46,6 +46,7 @@ public class Post { var lastCommentedTime: String! var participantsCount: Int! var coverImageMedium: String? + var acceptedAnswer: DiscussionThreadAnswer? public required init?(map: Map) { } @@ -72,5 +73,6 @@ extension Post: TestpressModel { lastCommentedTime <- map["last_commented_time"] participantsCount <- map["participants_count"] coverImageMedium <- map["cover_image_medium"] + acceptedAnswer <- map["accepted_answer"] } } diff --git a/ios-app/UI/DiscussionThreadDetailViewController.swift b/ios-app/UI/DiscussionThreadDetailViewController.swift new file mode 100644 index 00000000..b7518007 --- /dev/null +++ b/ios-app/UI/DiscussionThreadDetailViewController.swift @@ -0,0 +1,53 @@ +// +// DiscussionThreadDetailViewController.swift +// ios-app +// +// Created by Karthik on 29/11/21. +// Copyright © 2021 Testpress. All rights reserved. +// + +import UIKit + +class DiscussionThreadDetailViewController: PostDetailViewController { + + override func getFormattedContent(_ post: Post?) -> String { + var html = WebViewUtils.getHeader() + getTitle() + + WebViewUtils.getHtmlContentWithMargin(post?.contentHtml ?? "") + + if (post?.acceptedAnswer != nil) { + html += "
" + html += WebViewUtils.getCommentHeadingTags(headingText: "Accepted Answer"); + html += WebViewUtils.getCommentItemTags(post!.acceptedAnswer!.comment) + } else { + html += "
" + } + + html += WebViewUtils.getCommentHeadingTags(headingText: Strings.COMMENTS); + + html += "" + + html += WebViewUtils.getLoadingProgressBar(className: "preview_comments_loading_layout") + html += "" + + html += "
" + html += WebViewUtils.getLoadingProgressBar(className: "new_comments_loading_layout", + visible: false) + + html += "" + + return html + } + + @IBAction func onReportClick(_ sender: Any) { + let vc = ReportDiscussionThreadViewController() + vc.discussionSlug = post.slug + self.present(vc, animated: true, completion: nil) + } +} diff --git a/ios-app/UI/ForumFilterViewController.swift b/ios-app/UI/ForumFilterViewController.swift new file mode 100644 index 00000000..f25131a9 --- /dev/null +++ b/ios-app/UI/ForumFilterViewController.swift @@ -0,0 +1,183 @@ +import Former + +class ForumFilterViewController: FormViewController { + var advancedFilters = [CustomCheckRowFormer]() + var sort = [CustomCheckRowFormer]() + var delegate: DiscussionFilterDelegate? + var filters = [String: Any]() + var categories = [Category]() + let categoryPager = CategoryPager() + let categoryPicker = InlinePickerRowFormer() {$0.titleLabel.text = "Choose Category"} + + override func viewDidLoad() { + super.viewDidLoad() + self.setStatusBarColor() + displayNavigationBar() + loadDiscussionCategories() + let categorySection = initializeCategoryFilter() + let filtersSection = initializeFilterSection() + let dateFilterSection = initializeDateFilterSection() + let sortSection = initializeSortSection() + former.append(sectionFormer: categorySection, filtersSection, + dateFilterSection, sortSection) + } + + + func displayNavigationBar() { + let width = self.view.frame.width + let navigationBar: UINavigationBar = UINavigationBar(frame: CGRect(x: 0, y: UIApplication.shared.statusBarFrame.height, width: width, height: 45)) + self.view.addSubview(navigationBar); + let navigationItem = UINavigationItem(title: "Filter Options") + let doneBtn = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done, target: nil, action: #selector(done)) + navigationItem.rightBarButtonItem = doneBtn + navigationBar.setItems([navigationItem], animated: false) + tableView.contentInset.top = navigationBar.bounds.height + 10 + } + + func loadDiscussionCategories() { + categoryPager.next(completion: { items, error in + self.categories.append(contentsOf: Array(items!.values)) + if self.categoryPager.hasMore { + self.loadDiscussionCategories() + } else { + self.categoryPicker.pickerItems.append(contentsOf: self.categories.map{InlinePickerItem(title: $0.name, value: String($0.id))}) + self.former.reload() + } + }) + } + + func initializeCategoryFilter() -> SectionFormer { + categoryPicker.configure { row in + row.pickerItems = [InlinePickerItem( + title: "", + displayTitle: NSAttributedString(string: "Not set"), + value: "")] + + self.categories.map { InlinePickerItem(title: $0.name) } + }.onValueChanged { item in + self.filters.updateValue(item.value!, forKey: "category") + } + return SectionFormer(rowFormer: categoryPicker) + .set(headerViewFormer: self.createSectionHeader(title: "FILTER BY CATEGORY")) + } + + func createSectionHeader(title: String) -> ViewFormer { + return LabelViewFormer() + .configure { + $0.text = title + $0.viewHeight = 50 + } + } + + func initializeFilterSection() -> SectionFormer { + let filters = [ + self.createFilter(name: "Posted by me", key: "posted_by_me"), + self.createFilter(name: "Commented by me", key: "commented_by_me"), + self.createFilter(name: "Upvoted by me", key: "upvoted_by_me") + ] + self.advancedFilters.append(contentsOf: filters) + advancedFilters.forEach { row in + row.onCheckChanged {value in + if (value) { + self.filters.updateValue("true", forKey: row.key!) + } else { + self.filters.removeValue(forKey: row.key!) + } + self.onFilterSelected(value: value, from: row, filters: self.advancedFilters) + } + } + return SectionFormer(rowFormers: self.advancedFilters) + .set(headerViewFormer: self.createSectionHeader(title:"FILTER BY")) + } + + func initializeDateFilterSection() -> SectionFormer { + let startDateRow = InlineDatePickerRowFormer() { + $0.titleLabel.text = "Discussions From" + }.inlineCellSetup { + $0.datePicker.datePickerMode = .date + }.displayTextFromDate{ + return String.mediumDateShortTime(date: $0) + }.onDateChanged{ + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + self.filters.updateValue(formatter.string(from: $0), forKey: "posted_after") + } + + let endDateRow = InlineDatePickerRowFormer() { + $0.titleLabel.text = "Discussions Till" + }.inlineCellSetup { + $0.datePicker.datePickerMode = .date + }.displayTextFromDate(String.mediumDateShortTime) + .onDateChanged{ + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + self.filters.updateValue(formatter.string(from: $0), forKey: "posted_before") + } + return SectionFormer(rowFormer: startDateRow, endDateRow) + .set(headerViewFormer: self.createSectionHeader(title: "FILTER BY DATE")) + } + + func initializeSortSection() -> SectionFormer { + let filters = [ + self.createFilter(name: "Recently added", key: "-created"), + self.createFilter(name: "Old to New", key: "created"), + self.createFilter(name: "Most Viewed", key: "-views_count"), + self.createFilter(name: "Most Upvoted", key: "-upvoted") + ] + self.sort.append(contentsOf:filters) + sort.forEach { row in + row.onCheckChanged {value in + self.filters.updateValue(row.key!, forKey: "sort") + self.onFilterSelected(value: value, from: row, filters: self.sort) + } + } + + return SectionFormer(rowFormers: self.sort) + .set(headerViewFormer: self.createSectionHeader(title:"Sort by")) + } + + @objc func done() { + self.delegate?.applyFilters(value: self.filters) + self.dismiss(animated: true, completion: nil) + } + + func onFilterSelected(value: Bool, from: CustomCheckRowFormer, filters: [CustomCheckRowFormer]) { + + if (value) { + filters.forEach { row in + row.checked = false + row.showOrHideCheckIcon() + } + from.checked = true + from.showOrHideCheckIcon() + } + } + + func createFilter(name: String, key: String) -> CustomCheckRowFormer { + let row = CustomCheckRowFormer{ + $0.titleLabel.text = name + } + + row.key = key + return row + } +} + + + +class CustomCheckRowFormer +: CheckRowFormer where T: CheckFormableRow { + var key: String? + + func showOrHideCheckIcon() { + if let customCheckView = customCheckView { + cell.accessoryView = customCheckView + customCheckView.isHidden = checked ? false : true + } else { + cell.accessoryType = checked ? .checkmark : .none + } + } +} + +protocol DiscussionFilterDelegate { + func applyFilters(value: [String: Any]) +} diff --git a/ios-app/UI/ForumTableViewCell.swift b/ios-app/UI/ForumTableViewCell.swift index 381e33e5..a1c21b18 100644 --- a/ios-app/UI/ForumTableViewCell.swift +++ b/ios-app/UI/ForumTableViewCell.swift @@ -78,8 +78,7 @@ class ForumTableViewCell: UITableViewCell { @objc func onItemClick() { let storyboard = UIStoryboard(name: Constants.POST_STORYBOARD, bundle: nil) - let viewController = storyboard.instantiateViewController(withIdentifier: - Constants.POST_DETAIL_VIEW_CONTROLLER) as! PostDetailViewController + let viewController = storyboard.instantiateViewController(withIdentifier:"DiscussionThreadDetailViewController") as! DiscussionThreadDetailViewController viewController.post = post viewController.forum = true diff --git a/ios-app/UI/ForumTableViewController.swift b/ios-app/UI/ForumTableViewController.swift index e28fff9f..1af2c9f5 100644 --- a/ios-app/UI/ForumTableViewController.swift +++ b/ios-app/UI/ForumTableViewController.swift @@ -45,4 +45,17 @@ class ForumTableViewController: TPBasePagedTableViewController { description: Strings.NO_FORUM_POSTS_DESCRIPTION) } + func filter(dict: [String: Any]) { + self.pager.reset() + for (k, v) in dict { + self.pager.queryParams.updateValue(v as! String, forKey: k) + } + self.refreshWithProgress() + } + + func search(searchString: String) { + self.pager.reset() + self.pager.queryParams.updateValue(searchString, forKey: "search") + self.refreshWithProgress() + } } diff --git a/ios-app/UI/ForumViewController.swift b/ios-app/UI/ForumViewController.swift index f689a504..238883f6 100644 --- a/ios-app/UI/ForumViewController.swift +++ b/ios-app/UI/ForumViewController.swift @@ -25,15 +25,19 @@ import UIKit -class ForumViewController: UIViewController { +class ForumViewController: UIViewController, DiscussionFilterDelegate { + @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var postCreateButton: UIButton! @IBOutlet weak var containerView: UIView! + let viewController = ForumFilterViewController() var forumTableViewController: ForumTableViewController! override func viewDidLoad() { super.viewDidLoad() + viewController.delegate = self + self.searchBar.delegate = self UIUtils.setButtonDropShadow(postCreateButton) } @@ -56,8 +60,32 @@ class ForumViewController: UIViewController { present(viewController, animated: true, completion: nil) } + func applyFilters(value: [String : Any]) { + if (value.count > 0) { + self.forumTableViewController.filter(dict: value) + } + } + + @IBAction func showProfileDetails(_ sender: UIBarButtonItem) { UIUtils.showProfileDetails(self) } + @IBAction func onFilterButtonClick(_ sender: Any) { + present(viewController, animated: true, completion: nil) + } + +} + + +extension ForumViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + Debounce.input(searchText, delay: 0.5, current: searchBar.text ?? "") {_ in + self.forumTableViewController.search(searchString: searchText) + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + self.forumTableViewController.search(searchString: "") + } } diff --git a/ios-app/UI/PostDetailViewController.swift b/ios-app/UI/PostDetailViewController.swift index 84d96422..fe761d67 100644 --- a/ios-app/UI/PostDetailViewController.swift +++ b/ios-app/UI/PostDetailViewController.swift @@ -114,7 +114,7 @@ class PostDetailViewController: BaseWebViewController, WKWebViewDelegate, WKScri self.loading = false self.post = post self.webView.loadHTMLString( - self.getFormattedContent(post!.contentHtml!), + self.getFormattedContent(post), baseURL: Bundle.main.bundleURL ) }) @@ -317,9 +317,9 @@ class PostDetailViewController: BaseWebViewController, WKWebViewDelegate, WKScri return WebViewUtils.getFormattedTitle(title: post.title) } - func getFormattedContent(_ contentHtml: String) -> String { + func getFormattedContent(_ post: Post?) -> String { var html = WebViewUtils.getHeader() + getTitle() + - WebViewUtils.getHtmlContentWithMargin(contentHtml) + WebViewUtils.getHtmlContentWithMargin(post?.contentHtml ?? "") html += "
" html += WebViewUtils.getCommentHeadingTags(headingText: Strings.COMMENTS); diff --git a/ios-app/UI/ReportDiscussionThread.swift b/ios-app/UI/ReportDiscussionThread.swift new file mode 100644 index 00000000..f9becf52 --- /dev/null +++ b/ios-app/UI/ReportDiscussionThread.swift @@ -0,0 +1,171 @@ + +import Former +import TTGSnackbar +import ToastSwiftFramework + +class ReportDiscussionThreadViewController: FormViewController { + var discussionSlug: String! + var options = [CustomCheckRowFormer]() + let reasonTextView = TextViewRowFormer().configure{ + $0.placeholder = "Please enter the reason" + } + + override func viewDidLoad() { + super.viewDidLoad() + self.setStatusBarColor() + displayNavigationBar() + let reasonsSection = initializeReasons() + let submitButtonSection = initializeSubmitButton() + former.append(sectionFormer: reasonsSection, submitButtonSection) + } + + func displayNavigationBar() { + let width = self.view.frame.width + let navigationBar: UINavigationBar = UINavigationBar(frame: CGRect(x: 0, y: UIApplication.shared.statusBarFrame.height, width: width, height: 45)) + self.view.addSubview(navigationBar); + let navigationItem = UINavigationItem(title: "Report discussion") + let doneBtn = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done, target: nil, action: #selector(done)) + doneBtn.title = "Cancel" + navigationItem.rightBarButtonItem = doneBtn + navigationBar.setItems([navigationItem], animated: false) + tableView.contentInset.top = navigationBar.bounds.height + 10 + } + + func initializeReasons() -> SectionFormer { + let filters = [ + self.createFilter(name: "Graphic violence", key: "Graphic violence"), + self.createFilter(name: "Hateful or abusive content", key: "Hateful or abusive content"), + self.createFilter(name: "Off-Topic", key: "Off-Topic"), + self.createFilter(name: "Inappropriate", key: "Inappropriate"), + self.createFilter(name: "Spam", key: "Spam"), + self.createFilter(name: "Something else", key: "reason") + ] + self.options.append(contentsOf: filters) + let sections = SectionFormer(rowFormers: self.options) + self.options.forEach { row in + row.onCheckChanged {value in + if (row.key == "reason" && value) { + self.insertSection(relate: sections)(true) + self.reasonTextView.text = nil + self.former.reload(rowFormer: self.reasonTextView) + } else { + self.insertSection(relate: sections)(false) + self.reasonTextView.text = row.key + } + self.onFilterSelected(value: value, from: row, filters: self.options) + } + } + return sections + } + + func createFilter(name: String, key: String) -> CustomCheckRowFormer { + let row = CustomCheckRowFormer{ + $0.titleLabel.text = name + } + + row.key = key + return row + } + + func onFilterSelected(value: Bool, from: CustomCheckRowFormer, filters: [CustomCheckRowFormer]) { + + if (value) { + filters.forEach { row in + row.checked = false + row.showOrHideCheckIcon() + } + from.checked = true + from.showOrHideCheckIcon() + } + } + + func initializeSubmitButton() -> SectionFormer { + let submitButton = LabelRowFormer() + .configure { + $0.text = "Submit" + } + submitButton.onSelected {[weak self] _ in + if (self?.reasonTextView.text == nil) { + TTGSnackbar(message: "You have not selected any reason", duration: .middle).show() + } else { + TTGSnackbar(message: (self?.reasonTextView.text!)!, duration: .middle).show() + } + } + + return SectionFormer(rowFormer: submitButton) + + } + + private lazy var subSectionFormer: SectionFormer = { + return SectionFormer(rowFormers: [reasonTextView]) + }() + + private func insertSection(relate: SectionFormer) -> (Bool) -> Void { + return { [weak self] insert in + guard let `self` = self else { return } + if insert { + self.former.insertUpdate(sectionFormers: [self.subSectionFormer], below: relate, rowAnimation: UITableView.RowAnimation.fade) + } else { + self.former.removeUpdate(sectionFormers: [self.subSectionFormer], rowAnimation: UITableView.RowAnimation.fade) + } + } + } + + func reportDiscussion(reason: String) { + TPApiClient.apiCall(endpointProvider: TPEndpointProvider(.logoutDevices), completion: { + _,error in + + if let error = error { + + return + } + }) + } + + @objc func done() { + self.dismiss(animated: true, completion: nil) + } +} + + + +final class CenterLabelCell: FormCell, LabelFormableRow { + func formTextLabel() -> UILabel? { + return titleLabel + } + + func formSubTextLabel() -> UILabel? { + return nil + } + + weak var titleLabel: UILabel! + + override func setup() { + super.setup() + self.backgroundColor = Colors.getRGB(Colors.PRIMARY) + + let titleLabel = UILabel() + titleLabel.textColor = Colors.getRGB(Colors.PRIMARY_TEXT) + titleLabel.font = .boldSystemFont(ofSize: 15) + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(titleLabel) + self.titleLabel = titleLabel + + let constraints = [ + NSLayoutConstraint.constraints( + withVisualFormat: "V:|-0-[titleLabel]-0-|", + options: [], + metrics: nil, + views: ["titleLabel": titleLabel] + ), + NSLayoutConstraint.constraints( + withVisualFormat: "H:|-0-[titleLabel]-0-|", + options: [], + metrics: nil, + views: ["titleLabel": titleLabel] + ) + ].flatMap { $0 } + contentView.addConstraints(constraints) + } +}