diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 89324a65a3..b5fccd51bd 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -503,6 +503,8 @@ 2C3ABA9123D229C200E90439 /* Bundle+Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3ABA9023D229C200E90439 /* Bundle+Version.swift */; }; 2C3DAB8D233D71B100453B1C /* StepFontSizeStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3DAB8C233D71B100453B1C /* StepFontSizeStorageManager.swift */; }; 2C3DAB91233D735E00453B1C /* StepFontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3DAB90233D735D00453B1C /* StepFontSize.swift */; }; + 2C41F0EC25BAE6F200DA634A /* StoryTemplatesNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C41F0EB25BAE6F200DA634A /* StoryTemplatesNetworkService.swift */; }; + 2C41F0F625BB007C00DA634A /* StoryOpenSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C41F0F525BB007C00DA634A /* StoryOpenSource.swift */; }; 2C42EFB62476F28B00423695 /* MagicLinksAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFB52476F28B00423695 /* MagicLinksAPI.swift */; }; 2C42EFBA2476FADB00423695 /* MagicLinksNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFB92476FADB00423695 /* MagicLinksNetworkService.swift */; }; 2C434D2C25B6F0C400854D6F /* StepikNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C434D2B25B6F0C400854D6F /* StepikNetworkService.swift */; }; @@ -2095,6 +2097,8 @@ 2C3D00C124C8080F00441053 /* Info-Release.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Release.plist"; sourceTree = ""; }; 2C3DAB8C233D71B100453B1C /* StepFontSizeStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepFontSizeStorageManager.swift; sourceTree = ""; }; 2C3DAB90233D735D00453B1C /* StepFontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepFontSize.swift; sourceTree = ""; }; + 2C41F0EB25BAE6F200DA634A /* StoryTemplatesNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTemplatesNetworkService.swift; sourceTree = ""; }; + 2C41F0F525BB007C00DA634A /* StoryOpenSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryOpenSource.swift; sourceTree = ""; }; 2C42EFB52476F28B00423695 /* MagicLinksAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicLinksAPI.swift; sourceTree = ""; }; 2C42EFB92476FADB00423695 /* MagicLinksNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicLinksNetworkService.swift; sourceTree = ""; }; 2C434D2B25B6F0C400854D6F /* StepikNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepikNetworkService.swift; sourceTree = ""; }; @@ -4932,6 +4936,7 @@ 08484EE6211AF42E0006266F /* SegmentedProgressView.swift */, 08484EED211AF4300006266F /* Story.swift */, 08D2F72C2122E4B5009BA052 /* StoryNavigationDelegate.swift */, + 2C41F0F525BB007C00DA634A /* StoryOpenSource.swift */, 080E1ACB212571D9006B58A9 /* StoryPartViewFactory.swift */, 2CA4CF99242A1825008BC4E8 /* OpenedStories */, 2CA4CF9A242A1831008BC4E8 /* Stories */, @@ -6850,6 +6855,7 @@ 2C5418CB24A2903700B2DCE2 /* StepikMetricsNetworkService.swift */, 62E987944A98FB989B36D72C /* StepsNetworkService.swift */, 2CF10C8A238426B300F8CC95 /* StepSourcesNetworkService.swift */, + 2C41F0EB25BAE6F200DA634A /* StoryTemplatesNetworkService.swift */, 2C6BBBBE22B26DB100889A45 /* SubmissionsNetworkService.swift */, 62E98E41865820B1B8F7357D /* UnitsNetworkService.swift */, 2C16495822C10DD300DF18CA /* UserActivitiesNetworkService.swift */, @@ -9436,6 +9442,7 @@ 62E983B402214CA82BF050E8 /* LessonsNetworkService.swift in Sources */, 2CA93080253C6B03007B717A /* DiscussionThreadPlainObject.swift in Sources */, 2CE9BF44248D07D0004F6659 /* CodeTemplatesPersistenceService.swift in Sources */, + 2C41F0EC25BAE6F200DA634A /* StoryTemplatesNetworkService.swift in Sources */, 2C8F3AE123CCBD26004D113A /* DownloadVideoQuality.swift in Sources */, 2CDB95892412C08100F676A7 /* SpotlightContinueUserActivityService.swift in Sources */, 62E98F5FB06328B5D7212700 /* HighlightFakeButton.swift in Sources */, @@ -9988,6 +9995,7 @@ 62E987084C26B24E039EA0A0 /* NotificationsPersistenceService.swift in Sources */, 62E98778B1320381E97A0E1F /* LastStepPersistenceService.swift in Sources */, 62E98672DA4B29B46167B0A4 /* LastCodeLanguagePersistenceService.swift in Sources */, + 2C41F0F625BB007C00DA634A /* StoryOpenSource.swift in Sources */, 46115BB4590732AAB387046D /* NewProfileUserActivityAssembly.swift in Sources */, 8A248387CF216CCCFFC39E76 /* NewProfileUserActivityDataFlow.swift in Sources */, 9FAD76150240B4F8C13ADFA2 /* NewProfileUserActivityInteractor.swift in Sources */, diff --git a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift index 74c489e53a..15bdde8578 100644 --- a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift +++ b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift @@ -665,13 +665,17 @@ extension AnalyticsEvent { // MARK: - Stories - - static func storyOpened(id: Int) -> AmplitudeAnalyticsEvent { - AmplitudeAnalyticsEvent( - name: "Story opened", - parameters: [ - "id": id - ] - ) + static func storyOpened(id: Int, source: StoryOpenSource) -> AmplitudeAnalyticsEvent { + var parameters: [String: Any] = [ + "id": id, + "source": source.name + ] + + if case .deeplink(let path) = source { + parameters["deeplink_url"] = path + } + + return AmplitudeAnalyticsEvent(name: "Story opened", parameters: parameters) } static func storyPartOpened(id: Int, position: Int) -> AmplitudeAnalyticsEvent { @@ -786,6 +790,8 @@ extension AnalyticsEvent { return "coursePromo" case .certificates: return "certificates" + case .story: + return "story-template" } }() diff --git a/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesAssembly.swift b/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesAssembly.swift index 4c331e60f0..46e09c9bd0 100644 --- a/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesAssembly.swift +++ b/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesAssembly.swift @@ -13,10 +13,17 @@ final class OpenedStoriesAssembly: Assembly { private let stories: [Story] private let startPosition: Int + private let storyOpenSource: StoryOpenSource - init(stories: [Story], startPosition: Int, moduleOutput: OpenedStoriesOutputProtocol?) { + init( + stories: [Story], + startPosition: Int, + storyOpenSource: StoryOpenSource, + moduleOutput: OpenedStoriesOutputProtocol? = nil + ) { self.stories = stories self.startPosition = startPosition + self.storyOpenSource = storyOpenSource self.moduleOutput = moduleOutput } @@ -28,7 +35,9 @@ final class OpenedStoriesAssembly: Assembly { ) let presenter = OpenedStoriesPresenter( view: viewController, - stories: self.stories, startPosition: self.startPosition, + stories: self.stories, + startPosition: self.startPosition, + storyOpenSource: self.storyOpenSource, analytics: StepikAnalytics.shared ) diff --git a/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesPresenter.swift b/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesPresenter.swift index eff5aa9369..afbfcf492d 100644 --- a/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesPresenter.swift +++ b/Stepic/Legacy/Controllers/Stories/OpenedStories/OpenedStoriesPresenter.swift @@ -57,12 +57,20 @@ class OpenedStoriesPresenter: OpenedStoriesPresenterProtocol { var currentPosition: Int + private let storyOpenSource: StoryOpenSource private let analytics: Analytics - init(view: OpenedStoriesViewProtocol, stories: [Story], startPosition: Int, analytics: Analytics) { + init( + view: OpenedStoriesViewProtocol, + stories: [Story], + startPosition: Int, + storyOpenSource: StoryOpenSource, + analytics: Analytics + ) { self.view = view self.stories = stories self.currentPosition = startPosition + self.storyOpenSource = storyOpenSource self.analytics = analytics NotificationCenter.default.addObserver( @@ -98,7 +106,11 @@ class OpenedStoriesPresenter: OpenedStoriesPresenterProtocol { } private func makeModule(for story: Story) -> UIViewController { - StoryAssembly(story: story, navigationDelegate: self).makeModule() + StoryAssembly( + story: story, + storyOpenSource: self.storyOpenSource, + navigationDelegate: self + ).makeModule() } @objc diff --git a/Stepic/Legacy/Controllers/Stories/Stories/StoriesPresenter.swift b/Stepic/Legacy/Controllers/Stories/Stories/StoriesPresenter.swift index 5449cbe5f3..cca983c50d 100644 --- a/Stepic/Legacy/Controllers/Stories/Stories/StoriesPresenter.swift +++ b/Stepic/Legacy/Controllers/Stories/Stories/StoriesPresenter.swift @@ -63,15 +63,6 @@ final class StoriesPresenter: StoriesPresenterProtocol { self.view?.updateStory(index: index) } - private func isSupported(story: Story) -> Bool { - for part in story.parts { - if part.type == nil { - return false - } - } - return story.parts.count > 0 - } - func refresh() { self.view?.set(state: .loading) @@ -90,7 +81,7 @@ final class StoriesPresenter: StoriesPresenterProtocol { } strongSelf.stories = stories.filter { - strongSelf.isSupported(story: $0) + $0.isSupported }.sorted(by: { $0.position >= $1.position }).sorted(by: { diff --git a/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift b/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift index 29d8c6aad0..62417523ad 100644 --- a/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift +++ b/Stepic/Legacy/Controllers/Stories/Stories/StoriesViewController/StoriesViewController.swift @@ -100,6 +100,7 @@ final class StoriesViewController: UIViewController, ControllerWithStepikPlaceho let moduleToPresent = OpenedStoriesAssembly( stories: self.stories, startPosition: index, + storyOpenSource: .catalog, moduleOutput: self.presenter as? OpenedStoriesOutputProtocol ).makeModule() if DeviceInfo.current.isPad { diff --git a/Stepic/Legacy/Controllers/Stories/Story.swift b/Stepic/Legacy/Controllers/Stories/Story.swift index 24491fe332..40126eb7b2 100644 --- a/Stepic/Legacy/Controllers/Stories/Story.swift +++ b/Stepic/Legacy/Controllers/Stories/Story.swift @@ -17,6 +17,15 @@ final class Story: JSONSerializable { var parts: [StoryPart] var position: Int + var isSupported: Bool { + for part in self.parts { + if part.type == nil { + return false + } + } + return self.parts.count > 0 + } + required init(json: JSON) { let id = json["id"].intValue self.id = json["id"].intValue diff --git a/Stepic/Legacy/Controllers/Stories/Story/StoryAssembly.swift b/Stepic/Legacy/Controllers/Stories/Story/StoryAssembly.swift index 2397124bd6..a9f063803c 100644 --- a/Stepic/Legacy/Controllers/Stories/Story/StoryAssembly.swift +++ b/Stepic/Legacy/Controllers/Stories/Story/StoryAssembly.swift @@ -10,10 +10,16 @@ import UIKit final class StoryAssembly: Assembly { private let story: Story + private let storyOpenSource: StoryOpenSource private weak var navigationDelegate: StoryNavigationDelegate? - init(story: Story, navigationDelegate: StoryNavigationDelegate) { + init( + story: Story, + storyOpenSource: StoryOpenSource, + navigationDelegate: StoryNavigationDelegate + ) { self.story = story + self.storyOpenSource = storyOpenSource self.navigationDelegate = navigationDelegate } @@ -32,6 +38,7 @@ final class StoryAssembly: Assembly { urlNavigator: urlNavigator, navigationDelegate: self.navigationDelegate, storyPartsReactionsPersistenceService: StoryPartsReactionsPersistenceService(), + storyOpenSource: self.storyOpenSource, analytics: StepikAnalytics.shared ) diff --git a/Stepic/Legacy/Controllers/Stories/Story/StoryPresenter.swift b/Stepic/Legacy/Controllers/Stories/Story/StoryPresenter.swift index 1561a71b4b..65d9e5ae26 100644 --- a/Stepic/Legacy/Controllers/Stories/Story/StoryPresenter.swift +++ b/Stepic/Legacy/Controllers/Stories/Story/StoryPresenter.swift @@ -44,6 +44,8 @@ final class StoryPresenter: StoryPresenterProtocol { private var urlNavigator: URLNavigator private var story: Story + private let storyOpenSource: StoryOpenSource + private let storyPartsReactionsPersistenceService: StoryPartsReactionsPersistenceServiceProtocol private let analytics: Analytics @@ -67,6 +69,7 @@ final class StoryPresenter: StoryPresenterProtocol { urlNavigator: URLNavigator, navigationDelegate: StoryNavigationDelegate?, storyPartsReactionsPersistenceService: StoryPartsReactionsPersistenceServiceProtocol, + storyOpenSource: StoryOpenSource, analytics: Analytics ) { self.view = view @@ -75,6 +78,7 @@ final class StoryPresenter: StoryPresenterProtocol { self.navigationDelegate = navigationDelegate self.urlNavigator = urlNavigator self.storyPartsReactionsPersistenceService = storyPartsReactionsPersistenceService + self.storyOpenSource = storyOpenSource self.analytics = analytics } @@ -138,7 +142,7 @@ final class StoryPresenter: StoryPresenterProtocol { } func didAppear() { - self.analytics.send(.storyOpened(id: self.storyID)) + self.analytics.send(.storyOpened(id: self.storyID, source: self.storyOpenSource)) NotificationCenter.default.post(name: .storyDidAppear, object: nil, userInfo: ["id": self.storyID]) if self.shouldRestartSegment { diff --git a/Stepic/Legacy/Controllers/Stories/StoryOpenSource.swift b/Stepic/Legacy/Controllers/Stories/StoryOpenSource.swift new file mode 100644 index 0000000000..1afa1d922b --- /dev/null +++ b/Stepic/Legacy/Controllers/Stories/StoryOpenSource.swift @@ -0,0 +1,18 @@ +import Foundation + +enum StoryOpenSource { + case home + case catalog + case deeplink(path: String) + + var name: String { + switch self { + case .home: + return "home" + case .catalog: + return "catalog" + case .deeplink: + return "deeplink" + } + } +} diff --git a/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift index b1f9927e32..c6620c36e2 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/StoryTemplatesAPI.swift @@ -1,11 +1,3 @@ -// -// StoryTemplatesAPI.swift -// Stepic -// -// Created by Ostrenkiy on 16.08.2018. -// Copyright © 2018 Alex Karpov. All rights reserved. -// - import Alamofire import Foundation import PromiseKit @@ -41,4 +33,14 @@ final class StoryTemplatesAPI: APIEndpoint { } } } + + func retrieve(ids: [Int]) -> Promise<[Story]> { + self.retrieve.request( + requestEndpoint: self.name, + paramName: self.name, + ids: ids, + updating: [], + withManager: self.manager + ) + } } diff --git a/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift b/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift index b5fa243b93..c76fbbd823 100644 --- a/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift +++ b/Stepic/Legacy/Model/Network/RequestMakers/RetrieveRequestMaker.swift @@ -266,7 +266,11 @@ final class RetrieveRequestMaker { updating: [T], withManager manager: Alamofire.Session ) -> Promise<[T]> { - Promise { seal in + if ids.isEmpty { + return .value([]) + } + + return Promise { seal in self.request( requestEndpoint: requestEndpoint, paramName: paramName, diff --git a/Stepic/Legacy/Services/DeepLinks/DeepLinkRoute.swift b/Stepic/Legacy/Services/DeepLinks/DeepLinkRoute.swift index bd1c07b8d8..951eadaca5 100644 --- a/Stepic/Legacy/Services/DeepLinks/DeepLinkRoute.swift +++ b/Stepic/Legacy/Services/DeepLinks/DeepLinkRoute.swift @@ -21,6 +21,7 @@ enum DeepLinkRoute { case course(courseID: Int) case coursePromo(courseID: Int) case certificates(userID: Int) + case story(id: Int) var path: String { let path: String @@ -65,6 +66,8 @@ enum DeepLinkRoute { path = "course/\(courseID)/promo" case .certificates(let userID): path = "users/\(userID)/certificates" + case .story(let id): + path = "story-template/\(id)" } return "\(StepikApplicationsInfo.stepikURL)/\(path)" @@ -81,23 +84,24 @@ enum DeepLinkRoute { } if let match = Pattern.course.regex.firstMatch(in: path), - let courseIDString = match.captures[0], - let courseID = Int(courseIDString), + let courseIDStringValue = match.captures[0], + let courseID = Int(courseIDStringValue), match.matchedString == path { self = .course(courseID: courseID) return } if let match = Pattern.coursePromo.regex.firstMatch(in: path), - let courseIDString = match.captures[0], - let courseID = Int(courseIDString), + let courseIDStringValue = match.captures[0], + let courseID = Int(courseIDStringValue), match.matchedString == path { self = .coursePromo(courseID: courseID) return } if let match = Pattern.profile.regex.firstMatch(in: path), - let userIDString = match.captures[0], let userID = Int(userIDString), + let userIDStringValue = match.captures[0], + let userID = Int(userIDStringValue), match.matchedString == path { self = .profile(userID: userID) return @@ -110,16 +114,18 @@ enum DeepLinkRoute { } if let match = Pattern.syllabus.regex.firstMatch(in: path), - let courseIDString = match.captures[0], - let courseID = Int(courseIDString), + let courseIDStringValue = match.captures[0], + let courseID = Int(courseIDStringValue), match.matchedString == path { self = .syllabus(courseID: courseID) return } if let match = Pattern.lesson.regex.firstMatch(in: path), - let lessonIDString = match.captures[0], let lessonID = Int(lessonIDString), - let stepIDString = match.captures[1], let stepID = Int(stepIDString), + let lessonIDStringValue = match.captures[0], + let lessonID = Int(lessonIDStringValue), + let stepIDStringValue = match.captures[1], + let stepID = Int(stepIDStringValue), match.matchedString == path { let unitID = match.captures[2].flatMap { Int($0) } self = .lesson(lessonID: lessonID, stepID: stepID, unitID: unitID) @@ -127,9 +133,12 @@ enum DeepLinkRoute { } if let match = Pattern.discussions.regex.firstMatch(in: path), - let lessonIDString = match.captures[0], let lessonID = Int(lessonIDString), - let stepIDString = match.captures[1], let stepID = Int(stepIDString), - let discussionIDString = match.captures[2], let discussionID = Int(discussionIDString), + let lessonIDStringValue = match.captures[0], + let lessonID = Int(lessonIDStringValue), + let stepIDStringValue = match.captures[1], + let stepID = Int(stepIDStringValue), + let discussionIDStringValue = match.captures[2], + let discussionID = Int(discussionIDStringValue), match.matchedString == path { let unitID = match.captures[3].flatMap { Int($0) } self = .discussions(lessonID: lessonID, stepID: stepID, discussionID: discussionID, unitID: unitID) @@ -137,12 +146,16 @@ enum DeepLinkRoute { } if let match = Pattern.solutions.regex.firstMatch(in: path), - let lessonIDString = match.captures[0], let lessonID = Int(lessonIDString), - let stepIDString = match.captures[1], let stepID = Int(stepIDString), - let url = URL(string: path), let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), + let lessonIDStringValue = match.captures[0], + let lessonID = Int(lessonIDStringValue), + let stepIDStringValue = match.captures[1], + let stepID = Int(stepIDStringValue), + let url = URL(string: path), + let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = urlComponents.queryItems, let discussionQueryItem = queryItems.first(where: { $0.name == "discussion" }), - let discussionIDString = discussionQueryItem.value, let discussionID = Int(discussionIDString), + let discussionIDStringValue = discussionQueryItem.value, + let discussionID = Int(discussionIDStringValue), match.matchedString == path { let unitID = queryItems.first(where: { $0.name == "unit" })?.value.flatMap { Int($0) } self = .solutions(lessonID: lessonID, stepID: stepID, discussionID: discussionID, unitID: unitID) @@ -150,12 +163,21 @@ enum DeepLinkRoute { } if let match = Pattern.certificates.regex.firstMatch(in: path), - let userIDString = match.captures[0], let userID = Int(userIDString), + let userIDStringValue = match.captures[0], + let userID = Int(userIDStringValue), match.matchedString == path { self = .certificates(userID: userID) return } + if let match = Pattern.story.regex.firstMatch(in: path), + let storyIDStringValue = match.captures[0], + let storyID = Int(storyIDStringValue), + match.matchedString == path { + self = .story(id: storyID) + return + } + return nil } @@ -170,6 +192,7 @@ enum DeepLinkRoute { case discussions case solutions case certificates + case story var regex: Regex { try! Regex(string: self.pattern, options: [.ignoreCase]) @@ -202,6 +225,8 @@ enum DeepLinkRoute { return #"\#(stepik)\#(lesson)step\/(\d+)(?:\?discussion=(\d+))(?:\&unit=(\d+))?&thread=solutions.*"# case .certificates: return #"\#(stepik)users\/(\d+)\/certificates\/?\#(queryComponents)"# + case .story: + return #"\#(stepik)story-template\/(\d+)\/?\#(queryComponents)"# } } } diff --git a/Stepic/Legacy/Services/DeepLinks/DeepLinkRouter.swift b/Stepic/Legacy/Services/DeepLinks/DeepLinkRouter.swift index 1aedf7949d..33f082f875 100644 --- a/Stepic/Legacy/Services/DeepLinks/DeepLinkRouter.swift +++ b/Stepic/Legacy/Services/DeepLinks/DeepLinkRouter.swift @@ -290,6 +290,53 @@ final class DeepLinkRouter { return } + static func routeToStoryWithID( + _ id: Story.IdType, + urlPath: String, + completion: @escaping ([UIViewController]) -> Void + ) { + struct Holder { + static var networkService = StoryTemplatesNetworkService(storyTemplatesAPI: StoryTemplatesAPI()) + } + + Holder.networkService.fetch(id: id).then { storyOrNil -> Promise<(Story, [Story])> in + guard let story = storyOrNil else { + throw Error.fetchFailed + } + + return Holder.networkService.fetch( + language: ContentLanguageService().globalContentLanguage, + maxVersion: StepikApplicationsInfo.Versions.stories ?? 0, + isPublished: AuthInfo.shared.user?.profileEntity?.isStaff != true ? true : nil + ).map { (story, $0) } + }.done { deepLinkStory, allStories in + let resultStories = ( + allStories.contains(where: { $0.id == deepLinkStory.id }) ? allStories : (allStories + [deepLinkStory]) + ).filter { + $0.isSupported + }.sorted { + $0.position >= $1.position + }.sorted { + !($0.isViewed.value) || ($1.isViewed.value) + } + + guard let deepLinkStoryIndex = resultStories.firstIndex(where: { $0.id == deepLinkStory.id }) else { + return completion([]) + } + + let assembly = OpenedStoriesAssembly( + stories: resultStories, + startPosition: deepLinkStoryIndex, + storyOpenSource: .deeplink(path: urlPath), + moduleOutput: nil + ) + + completion([assembly.makeModule()]) + }.catch { _ in + completion([]) + } + } + static func routeToCatalogWithID(_ id: CourseListModel.IdType, completion: @escaping ([UIViewController]) -> Void) { struct Holder { static var persistenceService = CourseListsPersistenceService() @@ -497,4 +544,8 @@ final class DeepLinkRouter { }) } } + + enum Error: Swift.Error { + case fetchFailed + } } diff --git a/Stepic/Legacy/Services/DeepLinks/DeepLinkRoutingService.swift b/Stepic/Legacy/Services/DeepLinks/DeepLinkRoutingService.swift index 050cc981fd..d8629207c7 100644 --- a/Stepic/Legacy/Services/DeepLinks/DeepLinkRoutingService.swift +++ b/Stepic/Legacy/Services/DeepLinks/DeepLinkRoutingService.swift @@ -8,6 +8,7 @@ import Foundation import PromiseKit +import SVProgressHUD final class DeepLinkRoutingService { private var courseViewSource: AnalyticsEvent.CourseViewSource? @@ -53,9 +54,15 @@ final class DeepLinkRoutingService { fallbackPath: fallbackURLPath ) router.route() - }.catch { _ in - //TODO: Handle this - print("network error during routing, handle this") + }.catch { error in + print("DeepLinkRoutingService :: failed route = \(String(describing: route)), fallbackPath = \(fallbackURLPath), error = \(error)") + + if let routerError = error as? Error { + switch routerError { + case .failedRouteToStory: + SVProgressHUD.showError(withStatus: routerError.errorDescription) + } + } } } @@ -95,6 +102,22 @@ final class DeepLinkRoutingService { embedInNavigation: true, fallbackPath: fallbackPath ) + case .story: + if let sourceController = (source ?? self.currentNavigation?.topViewController), + let destinationController = moduleStack.first { + return ModalRouter( + source: sourceController, + destination: destinationController, + embedInNavigation: false + ) + } else { + return ModalOrPushStackRouter( + source: source, + destinationStack: moduleStack, + embedInNavigation: false, + fallbackPath: fallbackPath + ) + } } } @@ -170,6 +193,27 @@ final class DeepLinkRoutingService { ) case .certificates(let userID): seal.fulfill([CertificatesLegacyAssembly(userID: userID).makeModule()]) + case .story(let id): + SVProgressHUD.show() + DeepLinkRouter.routeToStoryWithID(id, urlPath: urlPath) { moduleStack in + if moduleStack.isEmpty { + seal.reject(Error.failedRouteToStory) + } else { + SVProgressHUD.showSuccess(withStatus: nil) + seal.fulfill(moduleStack) + } + } + } + } + } + + enum Error: Swift.Error, LocalizedError { + case failedRouteToStory + + var errorDescription: String? { + switch self { + case .failedRouteToStory: + return NSLocalizedString("DeepLinkRoutingServiceErrorStatusRouteStory", comment: "") } } } diff --git a/Stepic/Legacy/Services/Notifications/NotificationsService.swift b/Stepic/Legacy/Services/Notifications/NotificationsService.swift index c94f256c6d..8caefbe07c 100644 --- a/Stepic/Legacy/Services/Notifications/NotificationsService.swift +++ b/Stepic/Legacy/Services/Notifications/NotificationsService.swift @@ -78,6 +78,7 @@ final class NotificationsService { case achievementProgresses = "achievement-progresses" case retentionNextDay = "retention-next-day" case retentionThirdDay = "retention-third-day" + case storyTemplates = "story-templates" } } @@ -173,22 +174,23 @@ extension NotificationsService { func handleRemoteNotification(with userInfo: NotificationUserInfo) { print("remote notification received: DEBUG = \(userInfo)") - guard let notificationType = self.extractNotificationType(from: userInfo) else { + guard let notificationTypeStringValue = self.extractNotificationType(from: userInfo) else { return print("remote notification received: unable to parse notification type") } - self.reportReceivedNotificationWithType(notificationType) + self.reportReceivedNotificationWithType(notificationTypeStringValue) - // FIXME: Use `NotificationType` instead of raw values. - switch notificationType { - case NotificationType.notifications.rawValue: + switch NotificationType(rawValue: notificationTypeStringValue) { + case .notifications?: self.resolveRemoteNotificationsNotification(userInfo) - case NotificationType.notificationStatuses.rawValue: + case .notificationStatuses?: self.resolveRemoteNotificationStatusesNotification(userInfo) - case NotificationType.achievementProgresses.rawValue: + case .achievementProgresses?: self.resolveRemoteAchievementNotification(userInfo) + case .storyTemplates?: + self.resolveRemoteStoryTemplatesNotification(userInfo) default: - print("remote notification received: unsupported notification type: \(notificationType)") + print("remote notification received: unsupported notification type: \(notificationTypeStringValue)") } } @@ -249,6 +251,16 @@ extension NotificationsService { self.routeToProfile(userInfo: userInfo) } + private func resolveRemoteStoryTemplatesNotification(_ userInfo: NotificationUserInfo) { + guard let storyURL = userInfo[PayloadKey.storyURL.rawValue] as? String else { + return print("remote notification received: unable to parse notification: \(userInfo)") + } + + DispatchQueue.main.async { + self.deepLinkRoutingService.route(path: storyURL) + } + } + private func routeToProfile(userInfo: NotificationUserInfo) { DispatchQueue.main.async { if #available(iOS 10.0, *) { @@ -278,6 +290,7 @@ extension NotificationsService { case id case new case badge + case storyURL = "story_url" } } diff --git a/Stepic/Sources/Services/Models/Network/StoryTemplatesNetworkService.swift b/Stepic/Sources/Services/Models/Network/StoryTemplatesNetworkService.swift new file mode 100644 index 0000000000..9be0b1c2d0 --- /dev/null +++ b/Stepic/Sources/Services/Models/Network/StoryTemplatesNetworkService.swift @@ -0,0 +1,34 @@ +import Foundation +import PromiseKit + +protocol StoryTemplatesNetworkServiceProtocol: AnyObject { + func fetch(ids: [Story.IdType]) -> Promise<[Story]> + func fetch(language: ContentLanguage, maxVersion: Int, isPublished: Bool?) -> Promise<[Story]> +} + +extension StoryTemplatesNetworkServiceProtocol { + func fetch(id: Story.IdType) -> Promise { + self.fetch(ids: [id]).then { stories -> Promise in + .value(stories.first) + } + } +} + +final class StoryTemplatesNetworkService: StoryTemplatesNetworkServiceProtocol { + private let storyTemplatesAPI: StoryTemplatesAPI + + init(storyTemplatesAPI: StoryTemplatesAPI) { + self.storyTemplatesAPI = storyTemplatesAPI + } + + func fetch(ids: [Story.IdType]) -> Promise<[Story]> { + self.storyTemplatesAPI.retrieve(ids: ids).then { stories -> Promise<[Story]> in + let reorderedStories = stories.reordered(order: ids, transform: { $0.id }) + return .value(reorderedStories) + } + } + + func fetch(language: ContentLanguage, maxVersion: Int, isPublished: Bool?) -> Promise<[Story]> { + self.storyTemplatesAPI.retrieve(isPublished: isPublished, language: language, maxVersion: maxVersion) + } +} diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 9d9e65dcf9..ecfa263f1a 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -316,6 +316,7 @@ NoNotifications = "No notifications"; AnonymousNotificationsTitle = "Anonymous users can't receive notifications"; SignInToHaveNotifications = "Sign in to be able to receive notifications"; BadConnectionAuth = "Check your internet connection"; +DeepLinkRoutingServiceErrorStatusRouteStory = "Failed to Open a Story"; ContinueLearningWidgetButtonTitle = "Continue Learning"; ShowAll = "All"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 53895350d2..bca5b52dc2 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -317,6 +317,7 @@ NoNotifications = "Нет уведомлений"; AnonymousNotificationsTitle = "Анонимные пользователи не могут получать уведомления"; SignInToHaveNotifications = "Войдите, чтобы получать уведомления"; BadConnectionAuth = "Проверьте ваше интернет-соединение"; +DeepLinkRoutingServiceErrorStatusRouteStory = "Не удалось открыть историю"; ContinueLearningWidgetButtonTitle = "Продолжить учиться"; ShowAll = "Все"; diff --git a/StepicTests/Sources/Tests/DeepLinkRouteTests.swift b/StepicTests/Sources/Tests/DeepLinkRouteTests.swift index 19521c0dde..570a9ed1f1 100644 --- a/StepicTests/Sources/Tests/DeepLinkRouteTests.swift +++ b/StepicTests/Sources/Tests/DeepLinkRouteTests.swift @@ -299,6 +299,23 @@ class DeepLinkRouteSpec: QuickSpec { } } } + + context("stories") { + it("matches story deep link with given paths") { + let paths = [ + "http://stepik.org/story-template/8092", + "https://stepik.org/story-template/8092", + "https://stepik.org/story-template/8092/", + "https://stepik.org/story-template/8092?etk=WzEyNSwyNTcwNzY2NV0.1j4TGN.KO8s5AxOSbpY3CxNa4X_OAUl5_o" + ] + self.checkPaths(paths) { route in + guard case let .story(id) = route else { + return .failed(reason: "wrong enum case, expected `story`, got \(route)") + } + return id == 8092 ? .succeeded : .failed(reason: "wrong course id") + } + } + } } } }