From 61c6f6bb110e40c3260a9f66b0d326dd3debceb8 Mon Sep 17 00:00:00 2001 From: Anton Yarmolenko Date: Mon, 30 Dec 2024 12:14:11 +0100 Subject: [PATCH] Sync: Part 10 sync to upstream (#556) * chore: add video and discussion analytics (#93) * fix: fixes after merge, deleted IAP parts * chore: changed dependency version, deleted unused analytics part * chore: regenerate mocks * chore: In app ratings modal and profile picture picture sheet UI improvements (#94) * chore: show subtitles for videos in full-screen mode (#97) * fix: after merge * fix: iOS 18 Toggle tap gesture (#98) * fix: added priority for toggle tap gesture * fix: fixed tap and drag actions for Toggle --------- Co-authored-by: Anton Yarmolenko <37253+rnr@users.noreply.github.com> * fix: gesture for ios 18.2 * chore: quick fix with performance for subtitles view --------- Co-authored-by: Saeed Bashir Co-authored-by: Anton Yarmolenko <37253+rnr@users.noreply.github.com> Co-authored-by: Shafqat Muneer --- .../Authorization.xcodeproj/project.pbxproj | 2 +- .../Presentation/Startup/StartupView.swift | 2 + .../Startup/StartupViewModel.swift | 2 +- Core/Core.xcodeproj/project.pbxproj | 2 +- Core/Core/Analytics/CoreAnalytics.swift | 41 ++++ .../AppReview/Elements/AppReviewButton.swift | 10 +- Course/Course.xcodeproj/project.pbxproj | 2 +- .../Course/Presentation/CourseAnalytics.swift | 60 +++++ .../CourseVideoDownloadBarView.swift | 23 +- .../Video/EncodedVideoPlayer.swift | 14 +- .../Video/EncodedVideoPlayerViewModel.swift | 4 +- .../Video/PlayerViewController.swift | 68 +++++- .../Video/PlayerViewControllerHolder.swift | 8 +- .../Presentation/Video/SubtitlesView.swift | 6 +- .../Video/VideoPlayerViewModel.swift | 88 +++++++- .../Video/YouTubeVideoPlayer.swift | 4 +- .../YoutubePlayerViewControllerHolder.swift | 4 + Course/CourseTests/CourseMock.generated.swift | 108 +++++++++ .../Unit/VideoPlayerViewModelTests.swift | 40 +++- Dashboard/Dashboard.xcodeproj/project.pbxproj | 2 +- Discovery/Discovery.xcodeproj/project.pbxproj | 2 +- .../Discussion.xcodeproj/project.pbxproj | 2 +- .../Base/BaseResponsesViewModel.swift | 129 ++++++++++- .../Comments/Responses/ResponsesView.swift | 20 +- .../Responses/ResponsesViewModel.swift | 33 ++- .../Comments/Thread/ThreadView.swift | 23 +- .../Comments/Thread/ThreadViewModel.swift | 35 ++- .../CreateNewThread/CreateNewThreadView.swift | 11 +- .../CreateNewThreadViewModel.swift | 23 +- .../Presentation/DiscussionAnalytics.swift | 101 +++++++++ .../Presentation/DiscussionRouter.swift | 2 + .../DiscussionMock.generated.swift | 157 ++++++++++++- .../Base/BaseResponsesViewModelTests.swift | 169 ++++++++++++-- .../Comment/ThreadViewModelTests.swift | 30 ++- .../CreateNewThreadViewModelTests.swift | 24 +- .../Responses/ResponsesViewModelTests.swift | 42 ++-- OpenEdX.xcodeproj/project.pbxproj | 4 +- OpenEdX/DI/ScreenAssembly.swift | 21 +- .../AnalyticsManager/AnalyticsManager.swift | 209 +++++++++++++++++- .../DeepLinkManager/DeepLinkManager.swift | 2 + .../DeepLinkRouter/DeepLinkRouter.swift | 4 + OpenEdX/Router.swift | 3 +- Profile/Profile.xcodeproj/project.pbxproj | 2 +- .../EditProfile/ProfileBottomSheet.swift | 13 +- WhatsNew/WhatsNew.xcodeproj/project.pbxproj | 2 +- 45 files changed, 1427 insertions(+), 126 deletions(-) diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index cac4d0445..8210e13c9 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -1605,7 +1605,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Authorization/Authorization/Presentation/Startup/StartupView.swift b/Authorization/Authorization/Presentation/Startup/StartupView.swift index 248eef3da..32c73b673 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupView.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupView.swift @@ -62,6 +62,7 @@ public struct StartupView: View { searchQuery: searchQuery, sourceScreen: .startup ) + viewModel.logAnalytics(searchQuery: searchQuery) }) .autocapitalization(.none) .autocorrectionDisabled() @@ -89,6 +90,7 @@ public struct StartupView: View { searchQuery: searchQuery, sourceScreen: .startup ) + viewModel.logAnalytics() } label: { Text(AuthLocalization.Startup.exploreAllCourses) .underline() diff --git a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift index 813025468..68691ff69 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift @@ -26,7 +26,7 @@ public class StartupViewModel: ObservableObject { self.config = config } - func logAnalytics(searchQuery: String?) { + func logAnalytics(searchQuery: String? = nil) { if let searchQuery { analytics.trackEvent( .logistrationCoursesSearch, diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 296f61cf1..ce7911eb4 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -2331,7 +2331,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Core/Core/Analytics/CoreAnalytics.swift b/Core/Core/Analytics/CoreAnalytics.swift index b74ec00ca..f9786d80d 100644 --- a/Core/Core/Analytics/CoreAnalytics.swift +++ b/Core/Core/Analytics/CoreAnalytics.swift @@ -129,6 +129,11 @@ public enum AnalyticsEvent: String { case bulkDownloadVideosToggle = "Video:Bulk Download Toggle" case bulkDownloadVideosSubsection = "Video:Bulk Download Subsection" case bulkDeleteVideosSubsection = "Videos:Delete Subsection Videos" + case videoLoaded = "Video:Loaded" + case videoSpeedChange = "Video:Change Speed" + case videoPlayed = "Video:Played" + case videoPaused = "Video:Paused" + case videoCompleted = "Video:Completed" case discussionAllPostsClicked = "Discussion:All Posts Clicked" case discussionFollowingClicked = "Discussion:Following Posts Clicked" case discussionTopicClicked = "Discussion:Topic Clicked" @@ -143,6 +148,12 @@ public enum AnalyticsEvent: String { case logistrationSignIn = "Logistration:Sign In" case logistrationRegister = "Logistration:Register" case profileEdit = "Profile:Edit Profile" + case discussionPostCreated = "Discussion:Post Created" + case discussionResponseAdded = "Discussion:Response Added" + case discussionCommentAdded = "Discussion:Comment Added" + case discussionFollowToggle = "Dicussion:Post Follow Toggle" + case discussionLikeToggle = "Discussion:Like Toggle" + case discussionReportToggle = "Discussion:Report Toggle" } public enum EventBIValue: String { @@ -192,6 +203,11 @@ public enum EventBIValue: String { case bulkDownloadVideosToggle = "edx.bi.app.videos.download.toggle" case bulkDownloadVideosSubsection = "edx.bi.video.subsection.bulkdownload" case bulkDeleteVideosSubsection = "edx.bi.app.video.delete.subsection" + case videoLoaded = "edx.bi.app.videos.loaded" + case videoSpeedChange = "edx.bi.app.videos.speed.changed" + case videoPlayed = "edx.bi.app.videos.played" + case videoPaused = "edx.bi.app.videos.paused" + case videoCompleted = "edx.bi.app.videos.completed" case dashboardCourseClicked = "edx.bi.app.course.dashboard" case courseOutlineVideosTabClicked = "edx.bi.app.course.video_tab" case courseOutlineOfflineTabClicked = "edx.bi.app.course.offline_tab" @@ -231,6 +247,12 @@ public enum EventBIValue: String { case logistrationSignIn = "edx.bi.app.logistration.signin" case logistrationRegister = "edx.bi.app.logistration.register" case profileEdit = "edx.bi.app.profile.edit" + case discussionPostCreated = "edx.bi.app.discussion.post_created" + case discussionResponseAdded = "edx.bi.app.discussion.response_added" + case discussionCommentAdded = "edx.bi.app.discussion.comment_added" + case discussionFollowToggle = "edx.bi.app.discussion.follow_toggle" + case discussionLikeToggle = "edx.bi.app.discussion.like_toggle" + case discussionReportToggle = "edx.bi.app.discussion.report_toggle" } public struct EventParamKey { @@ -267,6 +289,25 @@ public struct EventParamKey { public static let pacing = "pacing" public static let dialog = "dialog" public static let snackbar = "snackbar" + public static let error = "error" + public static let errorAction = "error_action" + public static let flowType = "flow_type" + public static let alertType = "alert_type" + public static let videoURL = "video_url" + public static let oldSpeed = "old_speed" + public static let newSpeed = "new_speed" + public static let currentTime = "current_time" + public static let duration = "duration" + public static let postType = "post_type" + public static let followPost = "follow_post" + public static let threadID = "thread_id" + public static let responseID = "response_id" + public static let commentID = "comment_id" + public static let author = "author" + public static let follow = "follow" + public static let like = "like" + public static let report = "report" + public static let discussionType = "discussion_type" } public struct EventCategory { diff --git a/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift b/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift index fda848184..78486c676 100644 --- a/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift +++ b/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift @@ -34,7 +34,7 @@ struct AppReviewButton: View { : CoreLocalization.Review.Button.rateUs ) ) - .foregroundColor(isActive ? Color.white : Color.black.opacity(0.6)) + .foregroundColor(isActive ? Theme.Colors.primaryButtonTextColor : Color.black.opacity(0.6)) .font(Theme.Fonts.labelLarge) .padding(3) @@ -42,9 +42,11 @@ struct AppReviewButton: View { .padding(.vertical, 9) }.fixedSize() .background( - isActive - ? Theme.Colors.accentColor - : Theme.Colors.cardViewStroke + Theme.Shapes.buttonShape + .fill( + isActive ? Theme.Colors.accentColor + : Theme.Colors.cardViewStroke + ) ) .accessibilityElement(children: .ignore) .accessibilityLabel( diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index c07cbebd6..28e83f266 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -2086,7 +2086,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; CEBCA4322CC13CDE00076589 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */ = { diff --git a/Course/Course/Presentation/CourseAnalytics.swift b/Course/Course/Presentation/CourseAnalytics.swift index 28eb5918a..5036886c8 100644 --- a/Course/Course/Presentation/CourseAnalytics.swift +++ b/Course/Course/Presentation/CourseAnalytics.swift @@ -120,6 +120,36 @@ public protocol CourseAnalytics { subSectionID: String, videos: Int ) + + func videoLoaded(courseID: String, blockID: String, videoURL: String) + + func videoPlayed(courseID: String, blockID: String, videoURL: String) + + func videoSpeedChange( + courseID: String, + blockID: String, + videoURL: String, + oldSpeed: Float, + newSpeed: Float, + currentTime: Double, + duration: Double + ) + + func videoPaused( + courseID: String, + blockID: String, + videoURL: String, + currentTime: Double, + duration: Double + ) + + func videoCompleted( + courseID: String, + blockID: String, + videoURL: String, + currentTime: Double, + duration: Double + ) } #if DEBUG @@ -199,5 +229,35 @@ class CourseAnalyticsMock: CourseAnalytics { subSectionID: String, videos: Int ) {} + + public func videoLoaded(courseID: String, blockID: String, videoURL: String) {} + + public func videoPlayed(courseID: String, blockID: String, videoURL: String) {} + + public func videoSpeedChange( + courseID: String, + blockID: String, + videoURL: String, + oldSpeed: Float, + newSpeed: Float, + currentTime: Double, + duration: Double + ) {} + + public func videoPaused( + courseID: String, + blockID: String, + videoURL: String, + currentTime: Double, + duration: Double + ) {} + + public func videoCompleted( + courseID: String, + blockID: String, + videoURL: String, + currentTime: Double, + duration: Double + ) {} } #endif diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift index 8af9257e4..0f59eacf1 100644 --- a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift @@ -135,13 +135,24 @@ struct CourseVideoDownloadBarView: View { Toggle("", isOn: .constant(viewModel.isOn)) .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.toggleSwitchColor)) .padding(.trailing, 15) - .onTapGesture { - if !viewModel.isInternetAvaliable { - onNotInternetAvaliable?() - return + .simultaneousGesture( + DragGesture(minimumDistance: 20, coordinateSpace: .local).onEnded { _ in + toggleAction() } - Task { await viewModel.onToggle() } - } + ) + .simultaneousGesture( + TapGesture().onEnded { + toggleAction() + } + ) .accessibilityIdentifier("download_toggle") } + + private func toggleAction() { + if !viewModel.isInternetAvaliable { + onNotInternetAvaliable?() + return + } + Task { await viewModel.onToggle() } + } } diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 51e4ef799..a2d69340a 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -28,6 +28,7 @@ public struct EncodedVideoPlayer: View { @State private var isLoading: Bool = true @State private var isAnimating: Bool = false @State private var isOrientationChanged: Bool = false + @State private var subtitleText: String = "" @State var showAlert = false @State var alertMessage: String? { @@ -54,7 +55,10 @@ public struct EncodedVideoPlayer: View { VStack(spacing: 10) { HStack { VStack { - PlayerViewController(playerController: viewModel.controller) + PlayerViewController( + playerController: viewModel.controller, + subtitleText: $subtitleText + ) .aspectRatio(16 / 9, contentMode: .fit) .frame(minWidth: playerWidth(for: reader.size)) .cornerRadius(12) @@ -115,6 +119,10 @@ public struct EncodedVideoPlayer: View { viewModel.controller.player?.allowsExternalPlayback = true viewModel.controller.setNeedsStatusBarAppearanceUpdate() } + .onReceive(viewModel.$currentTime) { currentTime in + let subtitle = viewModel.findSubtitle(at: Date(milliseconds: currentTime)) + subtitleText = subtitle?.text ?? "" + } } private func playerWidth(for size: CGSize) -> CGFloat { @@ -138,7 +146,9 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), connectivity: Connectivity(), - playerHolder: PlayerViewControllerHolder.mock + playerHolder: PlayerViewControllerHolder.mock, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index b7bdaed9f..fa8125d7b 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -10,7 +10,7 @@ import Core import Combine public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { - var controller: AVPlayerViewController { - (playerHolder.playerController as? AVPlayerViewController) ?? AVPlayerViewController() + var controller: CustomAVPlayerViewController { + (playerHolder.playerController as? CustomAVPlayerViewController) ?? CustomAVPlayerViewController() } } diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 573a1195e..2549c68cf 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -11,9 +11,10 @@ import SwiftUI import _AVKit_SwiftUI struct PlayerViewController: UIViewControllerRepresentable { - var playerController: AVPlayerViewController + var playerController: CustomAVPlayerViewController + @Binding var subtitleText: String - func makeUIViewController(context: Context) -> AVPlayerViewController { + func makeUIViewController(context: Context) -> CustomAVPlayerViewController { do { try AVAudioSession.sharedInstance().setCategory(.playback) } catch { @@ -23,5 +24,66 @@ struct PlayerViewController: UIViewControllerRepresentable { return playerController } - func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {} + func updateUIViewController(_ playerController: CustomAVPlayerViewController, context: Context) { + playerController.subtitleText = subtitleText + } +} + +class CustomAVPlayerViewController: AVPlayerViewController { + private let subtitleLabel = UILabel() + + var subtitleText: String = "" { + didSet { + subtitleLabel.text = subtitleText + } + } + + var hideSubtitle: Bool = false { + didSet { + subtitleLabel.isHidden = hideSubtitle + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + // Configure the subtitle label + subtitleLabel.textColor = .white + subtitleLabel.font = UIFont.preferredFont(forTextStyle: .headline) + subtitleLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6) + subtitleLabel.textAlignment = .center + subtitleLabel.numberOfLines = 0 + subtitleLabel.layer.cornerRadius = 8 + subtitleLabel.layer.masksToBounds = true + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel.isHidden = true + + self.delegate = self + + // Add subtitle label to the content overlay view of AVPlayerViewController + contentOverlayView?.addSubview(subtitleLabel) + + // Set constraints for the subtitle label + NSLayoutConstraint.activate([ + subtitleLabel.centerXAnchor.constraint(equalTo: contentOverlayView!.centerXAnchor), + subtitleLabel.bottomAnchor.constraint(equalTo: contentOverlayView!.bottomAnchor, constant: -20), + subtitleLabel.widthAnchor.constraint(lessThanOrEqualTo: contentOverlayView!.widthAnchor, multiplier: 0.9) + ]) + } +} + +extension CustomAVPlayerViewController: @preconcurrency AVPlayerViewControllerDelegate { + func playerViewController( + _ playerViewController: AVPlayerViewController, + willBeginFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator + ) { + hideSubtitle = false + } + + func playerViewController( + _ playerViewController: AVPlayerViewController, + willEndFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator + ) { + hideSubtitle = true + } } diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index e6d994287..9c43dbff6 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -19,6 +19,7 @@ public protocol PlayerViewControllerHolderProtocol: AnyObject, Sendable { var isPlaying: Bool { get } var isPlayingInPip: Bool { get } var isOtherPlayerInPipPlaying: Bool { get } + var duration: TimeInterval { get } init( url: URL?, @@ -35,6 +36,7 @@ public protocol PlayerViewControllerHolderProtocol: AnyObject, Sendable { func getErrorPublisher() -> AnyPublisher func getRatePublisher() -> AnyPublisher func getReadyPublisher() -> AnyPublisher + func getFinishPublisher() -> AnyPublisher func getService() -> PlayerServiceProtocol func sendCompletion() async } @@ -80,7 +82,7 @@ public final class PlayerViewControllerHolder: PlayerViewControllerHolderProtoco let pipManager: PipManagerProtocol public lazy var playerController: PlayerControllerProtocol? = { - let playerController = AVPlayerViewController() + let playerController = CustomAVPlayerViewController() playerController.modalPresentationStyle = .fullScreen playerController.allowsPictureInPicturePlayback = true playerController.canStartPictureInPictureAutomaticallyFromInline = true @@ -199,6 +201,10 @@ public final class PlayerViewControllerHolder: PlayerViewControllerHolderProtoco public func getReadyPublisher() -> AnyPublisher { playerTracker.getReadyPublisher() } + + public func getFinishPublisher() -> AnyPublisher { + playerTracker.getFinishPublisher() + } public func getService() -> PlayerServiceProtocol { playerService diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index 8ce15c50e..5f7efc8a6 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -61,7 +61,7 @@ public struct SubtitlesView: View { ScrollView { if viewModel.subtitles.count > 0 { - VStack(alignment: .leading, spacing: 0) { + LazyVStack(alignment: .leading, spacing: 0) { ForEach(viewModel.subtitles, id: \.id) { subtitle in HStack { Button(action: { @@ -125,7 +125,9 @@ struct SubtittlesView_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), connectivity: Connectivity(), - playerHolder: PlayerViewControllerHolder.mock + playerHolder: PlayerViewControllerHolder.mock, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() ), scrollTo: {_ in } ) } diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index e5a67072e..db59437f4 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -40,17 +40,23 @@ public class VideoPlayerViewModel: ObservableObject { } public let playerHolder: PlayerViewControllerHolderProtocol internal var subscription = Set() + private var appStorage: CoreStorage? + private var analytics: CourseAnalytics? public init( languages: [SubtitleUrl], playerStateSubject: CurrentValueSubject? = nil, connectivity: ConnectivityProtocol, - playerHolder: PlayerViewControllerHolderProtocol + playerHolder: PlayerViewControllerHolderProtocol, + appStorage: CoreStorage?, + analytics: CourseAnalytics? ) { self.languages = languages self.connectivity = connectivity self.playerHolder = playerHolder self.prepareLanguages() + self.appStorage = appStorage + self.analytics = analytics observePlayer(with: playerStateSubject) } @@ -90,6 +96,20 @@ public class VideoPlayerViewModel: ObservableObject { .sink {[weak self] isReady in guard isReady else { return } self?.isLoading = false + self?.trackVideoLoaded() + } + .store(in: &subscription) + + playerHolder.getRatePublisher() + .sink {[weak self] rate in + guard self?.isLoading == false else { return } + self?.trackVideoSpeedChange(rate: rate) + } + .store(in: &subscription) + + playerHolder.getFinishPublisher() + .sink { [weak self] in + self?.trackVideoCompleted() } .store(in: &subscription) @@ -186,4 +206,70 @@ public class VideoPlayerViewModel: ObservableObject { }) } } + + private func trackVideoLoaded() { + analytics?.videoLoaded( + courseID: playerHolder.courseID, + blockID: playerHolder.blockID, + videoURL: playerHolder.url?.absoluteString ?? "" + ) + } + + private func trackVideoPlayed() { + analytics?.videoPlayed( + courseID: playerHolder.courseID, + blockID: playerHolder.blockID, + videoURL: playerHolder.url?.absoluteString ?? "" + ) + } + + private func trackVideoSpeedChange(rate: Float) { + if rate == 0 { + analytics?.videoPaused( + courseID: playerHolder.courseID, + blockID: playerHolder.blockID, + videoURL: playerHolder.url?.absoluteString ?? "", + currentTime: currentTime, + duration: playerHolder.duration + ) + } else { + guard let storage = appStorage, + let userSettings = storage.userSettings + else { + return + } + + if userSettings.videoPlaybackSpeed == rate { + analytics?.videoPlayed( + courseID: playerHolder.courseID, + blockID: playerHolder.blockID, + videoURL: playerHolder.url?.absoluteString ?? "" + ) + } else { + analytics?.videoSpeedChange( + courseID: playerHolder.courseID, + blockID: playerHolder.blockID, + videoURL: playerHolder.url?.absoluteString ?? "", + oldSpeed: userSettings.videoPlaybackSpeed, + newSpeed: rate, + currentTime: currentTime, + duration: playerHolder.duration + ) + } + } + } + + private func trackVideoCompleted() { + analytics?.videoCompleted( + courseID: playerHolder.courseID, + blockID: playerHolder.blockID, + videoURL: playerHolder.url?.absoluteString ?? "", + currentTime: currentTime, + duration: playerHolder.duration + ) + } + + func findSubtitle(at currentTime: Date) -> Subtitle? { + return subtitles.first { $0.fromTo.contains(currentTime) } + } } diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index 2374a4f14..5914a1807 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -89,7 +89,9 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), connectivity: Connectivity(), - playerHolder: YoutubePlayerViewControllerHolder.mock + playerHolder: YoutubePlayerViewControllerHolder.mock, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift index 52e65540a..2a28da9bf 100644 --- a/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift @@ -139,6 +139,10 @@ public final class YoutubePlayerViewControllerHolder: PlayerViewControllerHolder public func getReadyPublisher() -> AnyPublisher { playerTracker.getReadyPublisher() } + + public func getFinishPublisher() -> AnyPublisher { + playerTracker.getFinishPublisher() + } public func getService() -> PlayerServiceProtocol { playerService diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 35e6ce7c2..d19ad5f4e 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -3603,6 +3603,36 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { perform?(`courseID`, `subSectionID`, `videos`) } + open func videoLoaded(courseID: String, blockID: String, videoURL: String) { + addInvocation(.m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`))) + let perform = methodPerformValue(.m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `videoURL`) + } + + open func videoPlayed(courseID: String, blockID: String, videoURL: String) { + addInvocation(.m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`))) + let perform = methodPerformValue(.m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `videoURL`) + } + + open func videoSpeedChange(courseID: String, blockID: String, videoURL: String, oldSpeed: Float, newSpeed: Float, currentTime: Double, duration: Double) { + addInvocation(.m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`), Parameter.value(`oldSpeed`), Parameter.value(`newSpeed`), Parameter.value(`currentTime`), Parameter.value(`duration`))) + let perform = methodPerformValue(.m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`), Parameter.value(`oldSpeed`), Parameter.value(`newSpeed`), Parameter.value(`currentTime`), Parameter.value(`duration`))) as? (String, String, String, Float, Float, Double, Double) -> Void + perform?(`courseID`, `blockID`, `videoURL`, `oldSpeed`, `newSpeed`, `currentTime`, `duration`) + } + + open func videoPaused(courseID: String, blockID: String, videoURL: String, currentTime: Double, duration: Double) { + addInvocation(.m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`), Parameter.value(`currentTime`), Parameter.value(`duration`))) + let perform = methodPerformValue(.m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`), Parameter.value(`currentTime`), Parameter.value(`duration`))) as? (String, String, String, Double, Double) -> Void + perform?(`courseID`, `blockID`, `videoURL`, `currentTime`, `duration`) + } + + open func videoCompleted(courseID: String, blockID: String, videoURL: String, currentTime: Double, duration: Double) { + addInvocation(.m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`), Parameter.value(`currentTime`), Parameter.value(`duration`))) + let perform = methodPerformValue(.m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`videoURL`), Parameter.value(`currentTime`), Parameter.value(`duration`))) as? (String, String, String, Double, Double) -> Void + perform?(`courseID`, `blockID`, `videoURL`, `currentTime`, `duration`) + } + fileprivate enum MethodType { case m_resumeCourseClicked__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter, Parameter, Parameter) @@ -3630,6 +3660,11 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case m_bulkDownloadVideosToggle__courseID_courseIDaction_action(Parameter, Parameter) case m_bulkDownloadVideosSubsection__courseID_courseIDsectionID_sectionIDsubSectionID_subSectionIDvideos_videos(Parameter, Parameter, Parameter, Parameter) case m_bulkDeleteVideosSubsection__courseID_courseIDsubSectionID_subSectionIDvideos_videos(Parameter, Parameter, Parameter) + case m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(Parameter, Parameter, Parameter) + case m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(Parameter, Parameter, Parameter) + case m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter, Parameter) + case m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(Parameter, Parameter, Parameter, Parameter, Parameter) + case m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(Parameter, Parameter, Parameter, Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -3816,6 +3851,49 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSubsectionid, rhs: rhsSubsectionid, with: matcher), lhsSubsectionid, rhsSubsectionid, "subSectionID")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsVideos, rhs: rhsVideos, with: matcher), lhsVideos, rhsVideos, "videos")) return Matcher.ComparisonResult(results) + + case (.m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(let lhsCourseid, let lhsBlockid, let lhsVideourl), .m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(let rhsCourseid, let rhsBlockid, let rhsVideourl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsVideourl, rhs: rhsVideourl, with: matcher), lhsVideourl, rhsVideourl, "videoURL")) + return Matcher.ComparisonResult(results) + + case (.m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(let lhsCourseid, let lhsBlockid, let lhsVideourl), .m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(let rhsCourseid, let rhsBlockid, let rhsVideourl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsVideourl, rhs: rhsVideourl, with: matcher), lhsVideourl, rhsVideourl, "videoURL")) + return Matcher.ComparisonResult(results) + + case (.m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(let lhsCourseid, let lhsBlockid, let lhsVideourl, let lhsOldspeed, let lhsNewspeed, let lhsCurrenttime, let lhsDuration), .m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(let rhsCourseid, let rhsBlockid, let rhsVideourl, let rhsOldspeed, let rhsNewspeed, let rhsCurrenttime, let rhsDuration)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsVideourl, rhs: rhsVideourl, with: matcher), lhsVideourl, rhsVideourl, "videoURL")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldspeed, rhs: rhsOldspeed, with: matcher), lhsOldspeed, rhsOldspeed, "oldSpeed")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNewspeed, rhs: rhsNewspeed, with: matcher), lhsNewspeed, rhsNewspeed, "newSpeed")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCurrenttime, rhs: rhsCurrenttime, with: matcher), lhsCurrenttime, rhsCurrenttime, "currentTime")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDuration, rhs: rhsDuration, with: matcher), lhsDuration, rhsDuration, "duration")) + return Matcher.ComparisonResult(results) + + case (.m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(let lhsCourseid, let lhsBlockid, let lhsVideourl, let lhsCurrenttime, let lhsDuration), .m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(let rhsCourseid, let rhsBlockid, let rhsVideourl, let rhsCurrenttime, let rhsDuration)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsVideourl, rhs: rhsVideourl, with: matcher), lhsVideourl, rhsVideourl, "videoURL")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCurrenttime, rhs: rhsCurrenttime, with: matcher), lhsCurrenttime, rhsCurrenttime, "currentTime")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDuration, rhs: rhsDuration, with: matcher), lhsDuration, rhsDuration, "duration")) + return Matcher.ComparisonResult(results) + + case (.m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(let lhsCourseid, let lhsBlockid, let lhsVideourl, let lhsCurrenttime, let lhsDuration), .m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(let rhsCourseid, let rhsBlockid, let rhsVideourl, let rhsCurrenttime, let rhsDuration)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsVideourl, rhs: rhsVideourl, with: matcher), lhsVideourl, rhsVideourl, "videoURL")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCurrenttime, rhs: rhsCurrenttime, with: matcher), lhsCurrenttime, rhsCurrenttime, "currentTime")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDuration, rhs: rhsDuration, with: matcher), lhsDuration, rhsDuration, "duration")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -3847,6 +3925,11 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case let .m_bulkDownloadVideosToggle__courseID_courseIDaction_action(p0, p1): return p0.intValue + p1.intValue case let .m_bulkDownloadVideosSubsection__courseID_courseIDsectionID_sectionIDsubSectionID_subSectionIDvideos_videos(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_bulkDeleteVideosSubsection__courseID_courseIDsubSectionID_subSectionIDvideos_videos(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(p0, p1, p2, p3, p4, p5, p6): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + case let .m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + case let .m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue } } func assertionName() -> String { @@ -3876,6 +3959,11 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case .m_bulkDownloadVideosToggle__courseID_courseIDaction_action: return ".bulkDownloadVideosToggle(courseID:action:)" case .m_bulkDownloadVideosSubsection__courseID_courseIDsectionID_sectionIDsubSectionID_subSectionIDvideos_videos: return ".bulkDownloadVideosSubsection(courseID:sectionID:subSectionID:videos:)" case .m_bulkDeleteVideosSubsection__courseID_courseIDsubSectionID_subSectionIDvideos_videos: return ".bulkDeleteVideosSubsection(courseID:subSectionID:videos:)" + case .m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL: return ".videoLoaded(courseID:blockID:videoURL:)" + case .m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL: return ".videoPlayed(courseID:blockID:videoURL:)" + case .m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration: return ".videoSpeedChange(courseID:blockID:videoURL:oldSpeed:newSpeed:currentTime:duration:)" + case .m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration: return ".videoPaused(courseID:blockID:videoURL:currentTime:duration:)" + case .m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration: return ".videoCompleted(courseID:blockID:videoURL:currentTime:duration:)" } } } @@ -3919,6 +4007,11 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func bulkDownloadVideosToggle(courseID: Parameter, action: Parameter) -> Verify { return Verify(method: .m_bulkDownloadVideosToggle__courseID_courseIDaction_action(`courseID`, `action`))} public static func bulkDownloadVideosSubsection(courseID: Parameter, sectionID: Parameter, subSectionID: Parameter, videos: Parameter) -> Verify { return Verify(method: .m_bulkDownloadVideosSubsection__courseID_courseIDsectionID_sectionIDsubSectionID_subSectionIDvideos_videos(`courseID`, `sectionID`, `subSectionID`, `videos`))} public static func bulkDeleteVideosSubsection(courseID: Parameter, subSectionID: Parameter, videos: Parameter) -> Verify { return Verify(method: .m_bulkDeleteVideosSubsection__courseID_courseIDsubSectionID_subSectionIDvideos_videos(`courseID`, `subSectionID`, `videos`))} + public static func videoLoaded(courseID: Parameter, blockID: Parameter, videoURL: Parameter) -> Verify { return Verify(method: .m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(`courseID`, `blockID`, `videoURL`))} + public static func videoPlayed(courseID: Parameter, blockID: Parameter, videoURL: Parameter) -> Verify { return Verify(method: .m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(`courseID`, `blockID`, `videoURL`))} + public static func videoSpeedChange(courseID: Parameter, blockID: Parameter, videoURL: Parameter, oldSpeed: Parameter, newSpeed: Parameter, currentTime: Parameter, duration: Parameter) -> Verify { return Verify(method: .m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(`courseID`, `blockID`, `videoURL`, `oldSpeed`, `newSpeed`, `currentTime`, `duration`))} + public static func videoPaused(courseID: Parameter, blockID: Parameter, videoURL: Parameter, currentTime: Parameter, duration: Parameter) -> Verify { return Verify(method: .m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(`courseID`, `blockID`, `videoURL`, `currentTime`, `duration`))} + public static func videoCompleted(courseID: Parameter, blockID: Parameter, videoURL: Parameter, currentTime: Parameter, duration: Parameter) -> Verify { return Verify(method: .m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(`courseID`, `blockID`, `videoURL`, `currentTime`, `duration`))} } public struct Perform { @@ -4000,6 +4093,21 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func bulkDeleteVideosSubsection(courseID: Parameter, subSectionID: Parameter, videos: Parameter, perform: @escaping (String, String, Int) -> Void) -> Perform { return Perform(method: .m_bulkDeleteVideosSubsection__courseID_courseIDsubSectionID_subSectionIDvideos_videos(`courseID`, `subSectionID`, `videos`), performs: perform) } + public static func videoLoaded(courseID: Parameter, blockID: Parameter, videoURL: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_videoLoaded__courseID_courseIDblockID_blockIDvideoURL_videoURL(`courseID`, `blockID`, `videoURL`), performs: perform) + } + public static func videoPlayed(courseID: Parameter, blockID: Parameter, videoURL: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_videoPlayed__courseID_courseIDblockID_blockIDvideoURL_videoURL(`courseID`, `blockID`, `videoURL`), performs: perform) + } + public static func videoSpeedChange(courseID: Parameter, blockID: Parameter, videoURL: Parameter, oldSpeed: Parameter, newSpeed: Parameter, currentTime: Parameter, duration: Parameter, perform: @escaping (String, String, String, Float, Float, Double, Double) -> Void) -> Perform { + return Perform(method: .m_videoSpeedChange__courseID_courseIDblockID_blockIDvideoURL_videoURLoldSpeed_oldSpeednewSpeed_newSpeedcurrentTime_currentTimeduration_duration(`courseID`, `blockID`, `videoURL`, `oldSpeed`, `newSpeed`, `currentTime`, `duration`), performs: perform) + } + public static func videoPaused(courseID: Parameter, blockID: Parameter, videoURL: Parameter, currentTime: Parameter, duration: Parameter, perform: @escaping (String, String, String, Double, Double) -> Void) -> Perform { + return Perform(method: .m_videoPaused__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(`courseID`, `blockID`, `videoURL`, `currentTime`, `duration`), performs: perform) + } + public static func videoCompleted(courseID: Parameter, blockID: Parameter, videoURL: Parameter, currentTime: Parameter, duration: Parameter, perform: @escaping (String, String, String, Double, Double) -> Void) -> Perform { + return Perform(method: .m_videoCompleted__courseID_courseIDblockID_blockIDvideoURL_videoURLcurrentTime_currentTimeduration_duration(`courseID`, `blockID`, `videoURL`, `currentTime`, `duration`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift index a4b1f8033..4fb068644 100644 --- a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift @@ -37,7 +37,13 @@ final class VideoPlayerViewModelTests: XCTestCase { let tracker = PlayerTrackerProtocolMock(url: nil) let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock()) - let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) + let viewModel = VideoPlayerViewModel( + languages: [], + connectivity: connectivity, + playerHolder: playerHolder, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() + ) await viewModel.getSubtitles(subtitlesUrl: "url") @@ -61,7 +67,13 @@ final class VideoPlayerViewModelTests: XCTestCase { let tracker = PlayerTrackerProtocolMock(url: nil) let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock()) - let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) + let viewModel = VideoPlayerViewModel( + languages: [], + connectivity: connectivity, + playerHolder: playerHolder, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() + ) await viewModel.getSubtitles(subtitlesUrl: "url") @@ -80,7 +92,13 @@ final class VideoPlayerViewModelTests: XCTestCase { let tracker = PlayerTrackerProtocolMock(url: nil) let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock()) - let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) + let viewModel = VideoPlayerViewModel( + languages: [], + connectivity: connectivity, + playerHolder: playerHolder, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() + ) viewModel.languages = [ SubtitleUrl(language: "en", url: "url"), @@ -132,7 +150,13 @@ final class VideoPlayerViewModelTests: XCTestCase { let tracker = PlayerTrackerProtocolMock(url: nil) let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock()) - let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) + let viewModel = VideoPlayerViewModel( + languages: [], + connectivity: connectivity, + playerHolder: playerHolder, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() + ) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: NSError())) @@ -155,7 +179,13 @@ final class VideoPlayerViewModelTests: XCTestCase { let tracker = PlayerTrackerProtocolMock(url: nil) let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service, appStorage: CoreStorageMock()) - let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) + let viewModel = VideoPlayerViewModel( + languages: [], + connectivity: connectivity, + playerHolder: playerHolder, + appStorage: CoreStorageMock(), + analytics: CourseAnalyticsMock() + ) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: noInternetError)) diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index 3d950ad7b..aa0daa692 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -1539,7 +1539,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index d7ef9d351..b49a36a25 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -1623,7 +1623,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 06568f18f..3a9c819a2 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -1760,7 +1760,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index 16e4def50..5ed34cff0 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -47,21 +47,32 @@ public class BaseResponsesViewModel { internal let config: ConfigProtocol internal let storage: CoreStorage internal let addPostSubject = CurrentValueSubject(nil) + private let analytics: DiscussionAnalytics? init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, - storage: CoreStorage + storage: CoreStorage, + analytics: DiscussionAnalytics? ) { self.interactor = interactor self.router = router self.config = config self.storage = storage + self.analytics = analytics } @MainActor - public func vote(id: String, isThread: Bool, voted: Bool, index: Int?) async -> Bool { + public func vote( + id: String, + isThread: Bool, + voted: Bool, + index: Int?, + courseID: String, + responseID: String? = nil, + isComment: Bool = false + ) async -> Bool { if let index { toggleLike(index: index) } else { @@ -70,8 +81,17 @@ public class BaseResponsesViewModel { do { if isThread { try await interactor.voteThread(voted: voted, threadID: id) + trackThreadToggleLike(courseID: courseID) } else { try await interactor.voteResponse(voted: voted, responseID: id) + if let index { + trackResponseCommentToggleLike( + courseID: courseID, + post: postComments?.comments[index], + responseID: responseID, + isComment: isComment + ) + } } return true } catch let error { @@ -90,7 +110,15 @@ public class BaseResponsesViewModel { } @MainActor - public func flag(id: String, isThread: Bool, abuseFlagged: Bool, index: Int?) async -> Bool { + public func flag( + id: String, + isThread: Bool, + abuseFlagged: Bool, + index: Int?, + courseID: String, + responseID: String? = nil, + isComment: Bool = false + ) async -> Bool { if let index { postComments?.comments[index].abuseFlagged.toggle() } else { @@ -99,8 +127,17 @@ public class BaseResponsesViewModel { do { if isThread { try await interactor.flagThread(abuseFlagged: abuseFlagged, threadID: id) + trackThreadToggleReport(courseID: courseID) } else { try await interactor.flagComment(abuseFlagged: abuseFlagged, commentID: id) + if let index { + trackResponseToggleReport( + courseID: courseID, + post: postComments?.comments[index], + responseID: responseID, + isComment: isComment + ) + } } return true @@ -162,4 +199,90 @@ public class BaseResponsesViewModel { } } } + + func trackToggleFollowThread( + courseID: String, + threadID: String, + author: String, + follow: Bool + ) { + analytics?.discussionFollowToggle( + courseID: courseID, + threadID: threadID, + author: author, + follow: follow + ) + } + + func trackThreadToggleLike( + courseID: String + ) { + guard let postComments else { + return + } + + analytics?.discussionLikeToggle( + courseID: courseID, + threadID: postComments.threadID, + responseID: nil, + commentID: nil, + author: postComments.authorName, + discussionType: "thread", + like: postComments.voted + ) + } + + func trackThreadToggleReport( + courseID: String + ) { + guard let postComments else { return } + + analytics?.discussionReportToggle( + courseID: courseID, + threadID: postComments.threadID, + responseID: nil, + commentID: nil, + author: postComments.authorName, + discussionType: "thread", + report: postComments.abuseFlagged + ) + } + + func trackResponseToggleReport( + courseID: String, + post: Post?, + responseID: String? = nil, + isComment: Bool + ) { + guard let post else { return } + + analytics?.discussionReportToggle( + courseID: courseID, + threadID: post.threadID, + responseID: isComment ? responseID : post.commentID, + commentID: isComment ? post.commentID : nil, + author: post.authorName, + discussionType: isComment ? "comment" : "response", + report: post.abuseFlagged + ) + } + + func trackResponseCommentToggleLike( + courseID: String, + post: Post?, + responseID: String? = nil, + isComment: Bool + ) { + guard let post else { return } + + analytics?.discussionLikeToggle( + courseID: courseID, + threadID: post.threadID, + responseID: isComment ? responseID : post.commentID, + commentID: isComment ? post.commentID : nil, + author: post.authorName, + discussionType: isComment ? "comment" : "response", + like: post.voted + ) + } } diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index 99c677cd2..dffc67574 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -63,7 +63,8 @@ public struct ResponsesView: View { id: parentComment.commentID, isThread: false, voted: comments.voted, - index: nil + index: nil, + courseID: viewModel.courseID ) { viewModel.sendThreadLikeState() } @@ -75,7 +76,8 @@ public struct ResponsesView: View { id: parentComment.commentID, isThread: false, abuseFlagged: comments.abuseFlagged, - index: nil + index: nil, + courseID: viewModel.courseID ) { viewModel.sendThreadReportState() } @@ -112,7 +114,10 @@ public struct ResponsesView: View { id: comment.commentID, isThread: false, voted: comment.voted, - index: index + index: index, + courseID: viewModel.courseID, + responseID: parentComment.commentID, + isComment: true ) } }, @@ -122,7 +127,10 @@ public struct ResponsesView: View { id: comment.commentID, isThread: false, abuseFlagged: comment.abuseFlagged, - index: index + index: index, + courseID: viewModel.courseID, + responseID: parentComment.commentID, + isComment: true ) } }, @@ -244,11 +252,13 @@ public struct ResponsesView: View { struct ResponsesView_Previews: PreviewProvider { static var previews: some View { let viewModel = ResponsesViewModel( + courseID: "", interactor: DiscussionInteractor(repository: DiscussionRepositoryMock()), router: DiscussionRouterMock(), config: ConfigMock(), storage: CoreStorageMock(), - threadStateSubject: .init(nil) + threadStateSubject: .init(nil), + analytics: DiscussionAnalyticsMock() ) let post = Post( authorName: "Kirill", diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index b4f9b5002..4a91a88c4 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -15,16 +15,22 @@ public final class ResponsesViewModel: BaseResponsesViewModel, ObservableObject @Published var scrollTrigger: Bool = false private let threadStateSubject: CurrentValueSubject public var isBlackedOut: Bool = false + private let analytics: DiscussionAnalytics? + let courseID: String public init( + courseID: String, interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, storage: CoreStorage, - threadStateSubject: CurrentValueSubject + threadStateSubject: CurrentValueSubject, + analytics: DiscussionAnalytics? ) { + self.courseID = courseID self.threadStateSubject = threadStateSubject - super.init(interactor: interactor, router: router, config: config, storage: storage) + self.analytics = analytics + super.init(interactor: interactor, router: router, config: config, storage: storage, analytics: analytics) } func generateCommentsResponses(comments: [UserComment], parentComment: Post) -> Post? { @@ -61,6 +67,13 @@ public final class ResponsesViewModel: BaseResponsesViewModel, ObservableObject parentID: parentID) isShowProgress = false addPostSubject.send(newComment) + trackCommentAdded( + courseID: courseID, + threadID: threadID, + responseID: parentID ?? "", + commentID: newComment.commentID, + author: newComment.authorName + ) } catch let error { isShowProgress = false if error.isInternetError { @@ -155,4 +168,20 @@ public final class ResponsesViewModel: BaseResponsesViewModel, ObservableObject threadStateSubject.send(.postAdded(id: postComments.commentID)) } } + + private func trackCommentAdded( + courseID: String, + threadID: String, + responseID: String, + commentID: String, + author: String + ) { + analytics?.discussionCommentAdded( + courseID: courseID, + threadID: threadID, + responseID: responseID, + commentID: commentID, + author: author + ) + } } diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index 6b6d7a689..0611c3395 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -53,7 +53,8 @@ public struct ThreadView: View { id: comments.threadID, isThread: true, voted: comments.voted, - index: nil + index: nil, + courseID: thread.courseID ) { viewModel.sendPostLikedState() } @@ -65,7 +66,8 @@ public struct ThreadView: View { id: comments.threadID, isThread: true, abuseFlagged: comments.abuseFlagged, - index: nil + index: nil, + courseID: thread.courseID ) { viewModel.sendReportedState() } @@ -77,6 +79,12 @@ public struct ThreadView: View { following: comments.followed, threadID: comments.threadID ) { + viewModel.trackToggleFollowThread( + courseID: thread.courseID, + threadID: thread.id, + author: thread.author, + follow: viewModel.postComments?.followed ?? false + ) viewModel.sendPostFollowedState() } } @@ -108,7 +116,8 @@ public struct ThreadView: View { id: comment.commentID, isThread: false, voted: comment.voted, - index: index + index: index, + courseID: thread.courseID ) } }, @@ -118,12 +127,14 @@ public struct ThreadView: View { id: comment.commentID, isThread: false, abuseFlagged: comment.abuseFlagged, - index: index + index: index, + courseID: thread.courseID ) } }, onCommentsTap: { viewModel.router.showComments( + courseID: thread.courseID, commentID: comment.commentID, parentComment: comment, threadStateSubject: viewModel.threadStateSubject, @@ -168,6 +179,7 @@ public struct ThreadView: View { if let threadID = viewModel.postComments?.threadID { Task { await viewModel.postComment( + courseID: thread.courseID, threadID: threadID, rawBody: commentText, parentID: viewModel.postComments?.parentID @@ -293,7 +305,8 @@ struct CommentsView_Previews: PreviewProvider { router: DiscussionRouterMock(), config: ConfigMock(), storage: CoreStorageMock(), - postStateSubject: .init(nil) + postStateSubject: .init(nil), + analytics: DiscussionAnalyticsMock() ) ThreadView(thread: userThread, viewModel: vm) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index 6a3385012..d33842aa6 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -17,16 +17,20 @@ public final class ThreadViewModel: BaseResponsesViewModel, ObservableObject { private var cancellable: AnyCancellable? private let postStateSubject: CurrentValueSubject public var isBlackedOut: Bool = false + private let analytics: DiscussionAnalytics? public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, storage: CoreStorage, - postStateSubject: CurrentValueSubject + postStateSubject: CurrentValueSubject, + analytics: DiscussionAnalytics? ) { self.postStateSubject = postStateSubject - super.init(interactor: interactor, router: router, config: config, storage: storage) + self.analytics = analytics + + super.init(interactor: interactor, router: router, config: config, storage: storage, analytics: analytics) cancellable = threadStateSubject .receive(on: RunLoop.main) @@ -89,7 +93,12 @@ public final class ThreadViewModel: BaseResponsesViewModel, ObservableObject { } @MainActor - public func postComment(threadID: String, rawBody: String, parentID: String?) async { + public func postComment( + courseID: String, + threadID: String, + rawBody: String, + parentID: String? + ) async { isShowProgress = true do { let newComment = try await interactor.addCommentTo(threadID: threadID, @@ -97,6 +106,12 @@ public final class ThreadViewModel: BaseResponsesViewModel, ObservableObject { parentID: parentID) isShowProgress = false addPostSubject.send(newComment) + trackResponseAdded( + courseID: courseID, + threadID: threadID, + responseID: newComment.commentID, + author: newComment.authorName + ) } catch let error { isShowProgress = false if error.isInternetError { @@ -242,4 +257,18 @@ public final class ThreadViewModel: BaseResponsesViewModel, ObservableObject { comments.comments[index] = comment postComments = comments } + + private func trackResponseAdded( + courseID: String, + threadID: String, + responseID: String, + author: String + ) { + analytics?.discussionResponseAdded( + courseID: courseID, + threadID: threadID, + responseID: responseID, + author: author + ) + } } diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift index 39aa77378..e630c9b8d 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift @@ -174,6 +174,12 @@ public struct CreateNewThreadView: View { Task { if await viewModel.createNewThread(newThread: newThread) { onPostCreated() + viewModel.trackCreateNewThread( + courseID: courseID, + topicID: viewModel.selectedTopic, + postType: postType.rawValue, + followPost: followPost + ) } } } @@ -210,7 +216,10 @@ struct AddTopic_Previews: PreviewProvider { let vm = CreateNewThreadViewModel( interactor: DiscussionInteractor.mock, router: DiscussionRouterMock(), - config: ConfigMock()) + config: ConfigMock(), + analytics: DiscussionAnalyticsMock(), + storage: CoreStorageMock() + ) CreateNewThreadView( viewModel: vm, diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift index 3f986d705..688d0c62e 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift @@ -28,15 +28,21 @@ public class CreateNewThreadViewModel: ObservableObject { public let interactor: DiscussionInteractorProtocol public let router: DiscussionRouter public let config: ConfigProtocol + private let analytics: DiscussionAnalytics? + private let storage: CoreStorage? public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: ConfigProtocol + config: ConfigProtocol, + analytics: DiscussionAnalytics, + storage: CoreStorage? ) { self.interactor = interactor self.router = router self.config = config + self.analytics = analytics + self.storage = storage } @MainActor @@ -82,4 +88,19 @@ public class CreateNewThreadViewModel: ObservableObject { return false } } + + func trackCreateNewThread( + courseID: String, + topicID: String, + postType: String, + followPost: Bool + ) { + analytics?.discussionCreateNewPost( + courseID: courseID, + topicID: topicID, + postType: postType, + followPost: followPost, + author: storage?.user?.username ?? "" + ) + } } diff --git a/Discussion/Discussion/Presentation/DiscussionAnalytics.swift b/Discussion/Discussion/Presentation/DiscussionAnalytics.swift index 969994b4c..5768c0568 100644 --- a/Discussion/Discussion/Presentation/DiscussionAnalytics.swift +++ b/Discussion/Discussion/Presentation/DiscussionAnalytics.swift @@ -12,6 +12,57 @@ public protocol DiscussionAnalytics { func discussionAllPostsClicked(courseId: String, courseName: String) func discussionFollowingClicked(courseId: String, courseName: String) func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) + + func discussionCreateNewPost( + courseID: String, + topicID: String, + postType: String, + followPost: Bool, + author: String + ) + + func discussionResponseAdded( + courseID: String, + threadID: String, + responseID: String, + author: String + ) + + func discussionCommentAdded( + courseID: String, + threadID: String, + responseID: String, + commentID: String, + author: String + ) + + func discussionFollowToggle( + courseID: String, + threadID: String, + author: String, + follow: Bool + ) + + func discussionLikeToggle( + courseID: String, + threadID: String, + responseID: String?, + commentID: String?, + author: String, + discussionType: String, + like: Bool + ) + + func discussionReportToggle( + courseID: String, + threadID: String, + responseID: String?, + commentID: String?, + author: String, + discussionType: String, + report: Bool + ) + } #if DEBUG @@ -19,5 +70,55 @@ class DiscussionAnalyticsMock: DiscussionAnalytics { public func discussionAllPostsClicked(courseId: String, courseName: String) {} public func discussionFollowingClicked(courseId: String, courseName: String) {} public func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) {} + + public func discussionCreateNewPost( + courseID: String, + topicID: String, + postType: String, + followPost: Bool, + author: String + ) {} + + public func discussionResponseAdded( + courseID: String, + threadID: String, + responseID: String, + author: String + ) {} + + public func discussionCommentAdded( + courseID: String, + threadID: String, + responseID: String, + commentID: String, + author: String + ) {} + + public func discussionFollowToggle( + courseID: String, + threadID: String, + author: String, + follow: Bool + ) {} + + public func discussionLikeToggle( + courseID: String, + threadID: String, + responseID: String? = nil, + commentID: String? = nil, + author: String, + discussionType: String, + like: Bool + ) {} + + public func discussionReportToggle( + courseID: String, + threadID: String, + responseID: String? = nil, + commentID: String? = nil, + author: String, + discussionType: String, + report: Bool + ) {} } #endif diff --git a/Discussion/Discussion/Presentation/DiscussionRouter.swift b/Discussion/Discussion/Presentation/DiscussionRouter.swift index 27ea2067c..e2a1b3e9d 100644 --- a/Discussion/Discussion/Presentation/DiscussionRouter.swift +++ b/Discussion/Discussion/Presentation/DiscussionRouter.swift @@ -34,6 +34,7 @@ public protocol DiscussionRouter: BaseRouter { func showDiscussionsSearch(courseID: String, isBlackedOut: Bool) func showComments( + courseID: String, commentID: String, parentComment: Post, threadStateSubject: CurrentValueSubject, @@ -71,6 +72,7 @@ public class DiscussionRouterMock: BaseRouterMock, DiscussionRouter { public func showDiscussionsSearch(courseID: String, isBlackedOut: Bool) {} public func showComments( + courseID: String, commentID: String, parentComment: Post, threadStateSubject: CurrentValueSubject, diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index e55c19cd2..3cff451fc 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -3471,11 +3471,53 @@ open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { perform?(`courseId`, `courseName`, `topicId`, `topicName`) } + open func discussionCreateNewPost(courseID: String, topicID: String, postType: String, followPost: Bool, author: String) { + addInvocation(.m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(Parameter.value(`courseID`), Parameter.value(`topicID`), Parameter.value(`postType`), Parameter.value(`followPost`), Parameter.value(`author`))) + let perform = methodPerformValue(.m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(Parameter.value(`courseID`), Parameter.value(`topicID`), Parameter.value(`postType`), Parameter.value(`followPost`), Parameter.value(`author`))) as? (String, String, String, Bool, String) -> Void + perform?(`courseID`, `topicID`, `postType`, `followPost`, `author`) + } + + open func discussionResponseAdded(courseID: String, threadID: String, responseID: String, author: String) { + addInvocation(.m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`author`))) + let perform = methodPerformValue(.m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`author`))) as? (String, String, String, String) -> Void + perform?(`courseID`, `threadID`, `responseID`, `author`) + } + + open func discussionCommentAdded(courseID: String, threadID: String, responseID: String, commentID: String, author: String) { + addInvocation(.m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`commentID`), Parameter.value(`author`))) + let perform = methodPerformValue(.m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`commentID`), Parameter.value(`author`))) as? (String, String, String, String, String) -> Void + perform?(`courseID`, `threadID`, `responseID`, `commentID`, `author`) + } + + open func discussionFollowToggle(courseID: String, threadID: String, author: String, follow: Bool) { + addInvocation(.m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`author`), Parameter.value(`follow`))) + let perform = methodPerformValue(.m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`author`), Parameter.value(`follow`))) as? (String, String, String, Bool) -> Void + perform?(`courseID`, `threadID`, `author`, `follow`) + } + + open func discussionLikeToggle(courseID: String, threadID: String, responseID: String?, commentID: String?, author: String, discussionType: String, like: Bool) { + addInvocation(.m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`commentID`), Parameter.value(`author`), Parameter.value(`discussionType`), Parameter.value(`like`))) + let perform = methodPerformValue(.m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`commentID`), Parameter.value(`author`), Parameter.value(`discussionType`), Parameter.value(`like`))) as? (String, String, String?, String?, String, String, Bool) -> Void + perform?(`courseID`, `threadID`, `responseID`, `commentID`, `author`, `discussionType`, `like`) + } + + open func discussionReportToggle(courseID: String, threadID: String, responseID: String?, commentID: String?, author: String, discussionType: String, report: Bool) { + addInvocation(.m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`commentID`), Parameter.value(`author`), Parameter.value(`discussionType`), Parameter.value(`report`))) + let perform = methodPerformValue(.m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(Parameter.value(`courseID`), Parameter.value(`threadID`), Parameter.value(`responseID`), Parameter.value(`commentID`), Parameter.value(`author`), Parameter.value(`discussionType`), Parameter.value(`report`))) as? (String, String, String?, String?, String, String, Bool) -> Void + perform?(`courseID`, `threadID`, `responseID`, `commentID`, `author`, `discussionType`, `report`) + } + fileprivate enum MethodType { case m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(Parameter, Parameter, Parameter, Parameter) + case m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(Parameter, Parameter, Parameter, Parameter, Parameter) + case m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(Parameter, Parameter, Parameter, Parameter) + case m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(Parameter, Parameter, Parameter, Parameter, Parameter) + case m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(Parameter, Parameter, Parameter, Parameter) + case m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter, Parameter) + case m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -3498,6 +3540,62 @@ open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTopicid, rhs: rhsTopicid, with: matcher), lhsTopicid, rhsTopicid, "topicId")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTopicname, rhs: rhsTopicname, with: matcher), lhsTopicname, rhsTopicname, "topicName")) return Matcher.ComparisonResult(results) + + case (.m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(let lhsCourseid, let lhsTopicid, let lhsPosttype, let lhsFollowpost, let lhsAuthor), .m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(let rhsCourseid, let rhsTopicid, let rhsPosttype, let rhsFollowpost, let rhsAuthor)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTopicid, rhs: rhsTopicid, with: matcher), lhsTopicid, rhsTopicid, "topicID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPosttype, rhs: rhsPosttype, with: matcher), lhsPosttype, rhsPosttype, "postType")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFollowpost, rhs: rhsFollowpost, with: matcher), lhsFollowpost, rhsFollowpost, "followPost")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAuthor, rhs: rhsAuthor, with: matcher), lhsAuthor, rhsAuthor, "author")) + return Matcher.ComparisonResult(results) + + case (.m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(let lhsCourseid, let lhsThreadid, let lhsResponseid, let lhsAuthor), .m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(let rhsCourseid, let rhsThreadid, let rhsResponseid, let rhsAuthor)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsThreadid, rhs: rhsThreadid, with: matcher), lhsThreadid, rhsThreadid, "threadID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResponseid, rhs: rhsResponseid, with: matcher), lhsResponseid, rhsResponseid, "responseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAuthor, rhs: rhsAuthor, with: matcher), lhsAuthor, rhsAuthor, "author")) + return Matcher.ComparisonResult(results) + + case (.m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(let lhsCourseid, let lhsThreadid, let lhsResponseid, let lhsCommentid, let lhsAuthor), .m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(let rhsCourseid, let rhsThreadid, let rhsResponseid, let rhsCommentid, let rhsAuthor)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsThreadid, rhs: rhsThreadid, with: matcher), lhsThreadid, rhsThreadid, "threadID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResponseid, rhs: rhsResponseid, with: matcher), lhsResponseid, rhsResponseid, "responseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCommentid, rhs: rhsCommentid, with: matcher), lhsCommentid, rhsCommentid, "commentID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAuthor, rhs: rhsAuthor, with: matcher), lhsAuthor, rhsAuthor, "author")) + return Matcher.ComparisonResult(results) + + case (.m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(let lhsCourseid, let lhsThreadid, let lhsAuthor, let lhsFollow), .m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(let rhsCourseid, let rhsThreadid, let rhsAuthor, let rhsFollow)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsThreadid, rhs: rhsThreadid, with: matcher), lhsThreadid, rhsThreadid, "threadID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAuthor, rhs: rhsAuthor, with: matcher), lhsAuthor, rhsAuthor, "author")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFollow, rhs: rhsFollow, with: matcher), lhsFollow, rhsFollow, "follow")) + return Matcher.ComparisonResult(results) + + case (.m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(let lhsCourseid, let lhsThreadid, let lhsResponseid, let lhsCommentid, let lhsAuthor, let lhsDiscussiontype, let lhsLike), .m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(let rhsCourseid, let rhsThreadid, let rhsResponseid, let rhsCommentid, let rhsAuthor, let rhsDiscussiontype, let rhsLike)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsThreadid, rhs: rhsThreadid, with: matcher), lhsThreadid, rhsThreadid, "threadID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResponseid, rhs: rhsResponseid, with: matcher), lhsResponseid, rhsResponseid, "responseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCommentid, rhs: rhsCommentid, with: matcher), lhsCommentid, rhsCommentid, "commentID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAuthor, rhs: rhsAuthor, with: matcher), lhsAuthor, rhsAuthor, "author")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDiscussiontype, rhs: rhsDiscussiontype, with: matcher), lhsDiscussiontype, rhsDiscussiontype, "discussionType")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsLike, rhs: rhsLike, with: matcher), lhsLike, rhsLike, "like")) + return Matcher.ComparisonResult(results) + + case (.m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(let lhsCourseid, let lhsThreadid, let lhsResponseid, let lhsCommentid, let lhsAuthor, let lhsDiscussiontype, let lhsReport), .m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(let rhsCourseid, let rhsThreadid, let rhsResponseid, let rhsCommentid, let rhsAuthor, let rhsDiscussiontype, let rhsReport)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsThreadid, rhs: rhsThreadid, with: matcher), lhsThreadid, rhsThreadid, "threadID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResponseid, rhs: rhsResponseid, with: matcher), lhsResponseid, rhsResponseid, "responseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCommentid, rhs: rhsCommentid, with: matcher), lhsCommentid, rhsCommentid, "commentID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAuthor, rhs: rhsAuthor, with: matcher), lhsAuthor, rhsAuthor, "author")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDiscussiontype, rhs: rhsDiscussiontype, with: matcher), lhsDiscussiontype, rhsDiscussiontype, "discussionType")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsReport, rhs: rhsReport, with: matcher), lhsReport, rhsReport, "report")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -3507,6 +3605,12 @@ open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { case let .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + case let .m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + case let .m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(p0, p1, p2, p3, p4, p5, p6): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + case let .m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(p0, p1, p2, p3, p4, p5, p6): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue } } func assertionName() -> String { @@ -3514,6 +3618,12 @@ open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { case .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName: return ".discussionAllPostsClicked(courseId:courseName:)" case .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName: return ".discussionFollowingClicked(courseId:courseName:)" case .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName: return ".discussionTopicClicked(courseId:courseName:topicId:topicName:)" + case .m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author: return ".discussionCreateNewPost(courseID:topicID:postType:followPost:author:)" + case .m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author: return ".discussionResponseAdded(courseID:threadID:responseID:author:)" + case .m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author: return ".discussionCommentAdded(courseID:threadID:responseID:commentID:author:)" + case .m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow: return ".discussionFollowToggle(courseID:threadID:author:follow:)" + case .m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like: return ".discussionLikeToggle(courseID:threadID:responseID:commentID:author:discussionType:like:)" + case .m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report: return ".discussionReportToggle(courseID:threadID:responseID:commentID:author:discussionType:report:)" } } } @@ -3535,6 +3645,12 @@ open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { public static func discussionAllPostsClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func discussionFollowingClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func discussionTopicClicked(courseId: Parameter, courseName: Parameter, topicId: Parameter, topicName: Parameter) -> Verify { return Verify(method: .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(`courseId`, `courseName`, `topicId`, `topicName`))} + public static func discussionCreateNewPost(courseID: Parameter, topicID: Parameter, postType: Parameter, followPost: Parameter, author: Parameter) -> Verify { return Verify(method: .m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(`courseID`, `topicID`, `postType`, `followPost`, `author`))} + public static func discussionResponseAdded(courseID: Parameter, threadID: Parameter, responseID: Parameter, author: Parameter) -> Verify { return Verify(method: .m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(`courseID`, `threadID`, `responseID`, `author`))} + public static func discussionCommentAdded(courseID: Parameter, threadID: Parameter, responseID: Parameter, commentID: Parameter, author: Parameter) -> Verify { return Verify(method: .m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(`courseID`, `threadID`, `responseID`, `commentID`, `author`))} + public static func discussionFollowToggle(courseID: Parameter, threadID: Parameter, author: Parameter, follow: Parameter) -> Verify { return Verify(method: .m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(`courseID`, `threadID`, `author`, `follow`))} + public static func discussionLikeToggle(courseID: Parameter, threadID: Parameter, responseID: Parameter, commentID: Parameter, author: Parameter, discussionType: Parameter, like: Parameter) -> Verify { return Verify(method: .m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(`courseID`, `threadID`, `responseID`, `commentID`, `author`, `discussionType`, `like`))} + public static func discussionReportToggle(courseID: Parameter, threadID: Parameter, responseID: Parameter, commentID: Parameter, author: Parameter, discussionType: Parameter, report: Parameter) -> Verify { return Verify(method: .m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(`courseID`, `threadID`, `responseID`, `commentID`, `author`, `discussionType`, `report`))} } public struct Perform { @@ -3550,6 +3666,24 @@ open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { public static func discussionTopicClicked(courseId: Parameter, courseName: Parameter, topicId: Parameter, topicName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { return Perform(method: .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(`courseId`, `courseName`, `topicId`, `topicName`), performs: perform) } + public static func discussionCreateNewPost(courseID: Parameter, topicID: Parameter, postType: Parameter, followPost: Parameter, author: Parameter, perform: @escaping (String, String, String, Bool, String) -> Void) -> Perform { + return Perform(method: .m_discussionCreateNewPost__courseID_courseIDtopicID_topicIDpostType_postTypefollowPost_followPostauthor_author(`courseID`, `topicID`, `postType`, `followPost`, `author`), performs: perform) + } + public static func discussionResponseAdded(courseID: Parameter, threadID: Parameter, responseID: Parameter, author: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_discussionResponseAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDauthor_author(`courseID`, `threadID`, `responseID`, `author`), performs: perform) + } + public static func discussionCommentAdded(courseID: Parameter, threadID: Parameter, responseID: Parameter, commentID: Parameter, author: Parameter, perform: @escaping (String, String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_discussionCommentAdded__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_author(`courseID`, `threadID`, `responseID`, `commentID`, `author`), performs: perform) + } + public static func discussionFollowToggle(courseID: Parameter, threadID: Parameter, author: Parameter, follow: Parameter, perform: @escaping (String, String, String, Bool) -> Void) -> Perform { + return Perform(method: .m_discussionFollowToggle__courseID_courseIDthreadID_threadIDauthor_authorfollow_follow(`courseID`, `threadID`, `author`, `follow`), performs: perform) + } + public static func discussionLikeToggle(courseID: Parameter, threadID: Parameter, responseID: Parameter, commentID: Parameter, author: Parameter, discussionType: Parameter, like: Parameter, perform: @escaping (String, String, String?, String?, String, String, Bool) -> Void) -> Perform { + return Perform(method: .m_discussionLikeToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypelike_like(`courseID`, `threadID`, `responseID`, `commentID`, `author`, `discussionType`, `like`), performs: perform) + } + public static func discussionReportToggle(courseID: Parameter, threadID: Parameter, responseID: Parameter, commentID: Parameter, author: Parameter, discussionType: Parameter, report: Parameter, perform: @escaping (String, String, String?, String?, String, String, Bool) -> Void) -> Perform { + return Perform(method: .m_discussionReportToggle__courseID_courseIDthreadID_threadIDresponseID_responseIDcommentID_commentIDauthor_authordiscussionType_discussionTypereport_report(`courseID`, `threadID`, `responseID`, `commentID`, `author`, `discussionType`, `report`), performs: perform) + } } public func given(_ method: Given) { @@ -4563,10 +4697,10 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { perform?(`courseID`, `isBlackedOut`) } - open func showComments(commentID: String, parentComment: Post, threadStateSubject: CurrentValueSubject, isBlackedOut: Bool, animated: Bool) { - addInvocation(.m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter.value(`commentID`), Parameter.value(`parentComment`), Parameter>.value(`threadStateSubject`), Parameter.value(`isBlackedOut`), Parameter.value(`animated`))) - let perform = methodPerformValue(.m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter.value(`commentID`), Parameter.value(`parentComment`), Parameter>.value(`threadStateSubject`), Parameter.value(`isBlackedOut`), Parameter.value(`animated`))) as? (String, Post, CurrentValueSubject, Bool, Bool) -> Void - perform?(`commentID`, `parentComment`, `threadStateSubject`, `isBlackedOut`, `animated`) + open func showComments(courseID: String, commentID: String, parentComment: Post, threadStateSubject: CurrentValueSubject, isBlackedOut: Bool, animated: Bool) { + addInvocation(.m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter.value(`courseID`), Parameter.value(`commentID`), Parameter.value(`parentComment`), Parameter>.value(`threadStateSubject`), Parameter.value(`isBlackedOut`), Parameter.value(`animated`))) + let perform = methodPerformValue(.m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter.value(`courseID`), Parameter.value(`commentID`), Parameter.value(`parentComment`), Parameter>.value(`threadStateSubject`), Parameter.value(`isBlackedOut`), Parameter.value(`animated`))) as? (String, String, Post, CurrentValueSubject, Bool, Bool) -> Void + perform?(`courseID`, `commentID`, `parentComment`, `threadStateSubject`, `isBlackedOut`, `animated`) } open func createNewThread(courseID: String, selectedTopic: String, onPostCreated: @escaping () -> Void) { @@ -4683,7 +4817,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case m_showThreads__courseID_courseIDtopics_topicstitle_titletype_typeisBlackedOut_isBlackedOutanimated_animated(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter) case m_showThread__thread_threadpostStateSubject_postStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter, Parameter>, Parameter, Parameter) case m_showDiscussionsSearch__courseID_courseIDisBlackedOut_isBlackedOut(Parameter, Parameter) - case m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter, Parameter, Parameter>, Parameter, Parameter) + case m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(Parameter, Parameter, Parameter, Parameter>, Parameter, Parameter) case m_createNewThread__courseID_courseIDselectedTopic_selectedTopiconPostCreated_onPostCreated(Parameter, Parameter, Parameter<() -> Void>) case m_backToRoot__animated_animated(Parameter) case m_back__animated_animated(Parameter) @@ -4734,8 +4868,9 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIsblackedout, rhs: rhsIsblackedout, with: matcher), lhsIsblackedout, rhsIsblackedout, "isBlackedOut")) return Matcher.ComparisonResult(results) - case (.m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(let lhsCommentid, let lhsParentcomment, let lhsThreadstatesubject, let lhsIsblackedout, let lhsAnimated), .m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(let rhsCommentid, let rhsParentcomment, let rhsThreadstatesubject, let rhsIsblackedout, let rhsAnimated)): + case (.m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(let lhsCourseid, let lhsCommentid, let lhsParentcomment, let lhsThreadstatesubject, let lhsIsblackedout, let lhsAnimated), .m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(let rhsCourseid, let rhsCommentid, let rhsParentcomment, let rhsThreadstatesubject, let rhsIsblackedout, let rhsAnimated)): var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCommentid, rhs: rhsCommentid, with: matcher), lhsCommentid, rhsCommentid, "commentID")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParentcomment, rhs: rhsParentcomment, with: matcher), lhsParentcomment, rhsParentcomment, "parentComment")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsThreadstatesubject, rhs: rhsThreadstatesubject, with: matcher), lhsThreadstatesubject, rhsThreadstatesubject, "threadStateSubject")) @@ -4853,7 +4988,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case let .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_typeisBlackedOut_isBlackedOutanimated_animated(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_showThread__thread_threadpostStateSubject_postStateSubjectisBlackedOut_isBlackedOutanimated_animated(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_showDiscussionsSearch__courseID_courseIDisBlackedOut_isBlackedOut(p0, p1): return p0.intValue + p1.intValue - case let .m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + case let .m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_createNewThread__courseID_courseIDselectedTopic_selectedTopiconPostCreated_onPostCreated(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_backToRoot__animated_animated(p0): return p0.intValue case let .m_back__animated_animated(p0): return p0.intValue @@ -4880,7 +5015,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_typeisBlackedOut_isBlackedOutanimated_animated: return ".showThreads(courseID:topics:title:type:isBlackedOut:animated:)" case .m_showThread__thread_threadpostStateSubject_postStateSubjectisBlackedOut_isBlackedOutanimated_animated: return ".showThread(thread:postStateSubject:isBlackedOut:animated:)" case .m_showDiscussionsSearch__courseID_courseIDisBlackedOut_isBlackedOut: return ".showDiscussionsSearch(courseID:isBlackedOut:)" - case .m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated: return ".showComments(commentID:parentComment:threadStateSubject:isBlackedOut:animated:)" + case .m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated: return ".showComments(courseID:commentID:parentComment:threadStateSubject:isBlackedOut:animated:)" case .m_createNewThread__courseID_courseIDselectedTopic_selectedTopiconPostCreated_onPostCreated: return ".createNewThread(courseID:selectedTopic:onPostCreated:)" case .m_backToRoot__animated_animated: return ".backToRoot(animated:)" case .m_back__animated_animated: return ".back(animated:)" @@ -4921,7 +5056,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func showThreads(courseID: Parameter, topics: Parameter, title: Parameter, type: Parameter, isBlackedOut: Parameter, animated: Parameter) -> Verify { return Verify(method: .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_typeisBlackedOut_isBlackedOutanimated_animated(`courseID`, `topics`, `title`, `type`, `isBlackedOut`, `animated`))} public static func showThread(thread: Parameter, postStateSubject: Parameter>, isBlackedOut: Parameter, animated: Parameter) -> Verify { return Verify(method: .m_showThread__thread_threadpostStateSubject_postStateSubjectisBlackedOut_isBlackedOutanimated_animated(`thread`, `postStateSubject`, `isBlackedOut`, `animated`))} public static func showDiscussionsSearch(courseID: Parameter, isBlackedOut: Parameter) -> Verify { return Verify(method: .m_showDiscussionsSearch__courseID_courseIDisBlackedOut_isBlackedOut(`courseID`, `isBlackedOut`))} - public static func showComments(commentID: Parameter, parentComment: Parameter, threadStateSubject: Parameter>, isBlackedOut: Parameter, animated: Parameter) -> Verify { return Verify(method: .m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(`commentID`, `parentComment`, `threadStateSubject`, `isBlackedOut`, `animated`))} + public static func showComments(courseID: Parameter, commentID: Parameter, parentComment: Parameter, threadStateSubject: Parameter>, isBlackedOut: Parameter, animated: Parameter) -> Verify { return Verify(method: .m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(`courseID`, `commentID`, `parentComment`, `threadStateSubject`, `isBlackedOut`, `animated`))} public static func createNewThread(courseID: Parameter, selectedTopic: Parameter, onPostCreated: Parameter<() -> Void>) -> Verify { return Verify(method: .m_createNewThread__courseID_courseIDselectedTopic_selectedTopiconPostCreated_onPostCreated(`courseID`, `selectedTopic`, `onPostCreated`))} public static func backToRoot(animated: Parameter) -> Verify { return Verify(method: .m_backToRoot__animated_animated(`animated`))} public static func back(animated: Parameter) -> Verify { return Verify(method: .m_back__animated_animated(`animated`))} @@ -4958,8 +5093,8 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func showDiscussionsSearch(courseID: Parameter, isBlackedOut: Parameter, perform: @escaping (String, Bool) -> Void) -> Perform { return Perform(method: .m_showDiscussionsSearch__courseID_courseIDisBlackedOut_isBlackedOut(`courseID`, `isBlackedOut`), performs: perform) } - public static func showComments(commentID: Parameter, parentComment: Parameter, threadStateSubject: Parameter>, isBlackedOut: Parameter, animated: Parameter, perform: @escaping (String, Post, CurrentValueSubject, Bool, Bool) -> Void) -> Perform { - return Perform(method: .m_showComments__commentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(`commentID`, `parentComment`, `threadStateSubject`, `isBlackedOut`, `animated`), performs: perform) + public static func showComments(courseID: Parameter, commentID: Parameter, parentComment: Parameter, threadStateSubject: Parameter>, isBlackedOut: Parameter, animated: Parameter, perform: @escaping (String, String, Post, CurrentValueSubject, Bool, Bool) -> Void) -> Perform { + return Perform(method: .m_showComments__courseID_courseIDcommentID_commentIDparentComment_parentCommentthreadStateSubject_threadStateSubjectisBlackedOut_isBlackedOutanimated_animated(`courseID`, `commentID`, `parentComment`, `threadStateSubject`, `isBlackedOut`, `animated`), performs: perform) } public static func createNewThread(courseID: Parameter, selectedTopic: Parameter, onPostCreated: Parameter<() -> Void>, perform: @escaping (String, String, @escaping () -> Void) -> Void) -> Perform { return Perform(method: .m_createNewThread__courseID_courseIDselectedTopic_selectedTopiconPostCreated_onPostCreated(`courseID`, `selectedTopic`, `onPostCreated`), performs: perform) diff --git a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index 6835b166e..de389cfbd 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -66,19 +66,29 @@ final class BaseResponsesViewModelTests: XCTestCase { interactor: interactor, router: router, config: config, - storage: CoreStorageMock() + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() ) } func testVoteThreadSuccess() async throws { - + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false viewModel.postComments = post Given(interactor, .voteThread(voted: .any, threadID: .any, willProduce: {_ in})) - result = await viewModel.vote(id: "1", isThread: true, voted: true, index: 0) + result = await viewModel.vote(id: "1", isThread: true, voted: true, index: 0, courseID: "courseID") Verify(interactor, .voteThread(voted: .value(true), threadID: .value("1"))) @@ -90,7 +100,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteResponseSuccess() async throws { - + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -99,7 +118,7 @@ final class BaseResponsesViewModelTests: XCTestCase { Given(interactor, .voteResponse(voted: .any, responseID: .any, willProduce: {_ in})) - result = await viewModel.vote(id: "1", isThread: false, voted: true, index: 0) + result = await viewModel.vote(id: "1", isThread: false, voted: true, index: 0, courseID: "courseID") Verify(interactor, .voteResponse(voted: .value(true), responseID: .value("1"))) @@ -111,7 +130,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteParentThreadSuccess() async throws { - + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -119,7 +147,7 @@ final class BaseResponsesViewModelTests: XCTestCase { Given(interactor, .voteThread(voted: .any, threadID: .any, willProduce: {_ in})) - result = await viewModel.vote(id: "1", isThread: true, voted: true, index: nil) + result = await viewModel.vote(id: "1", isThread: true, voted: true, index: nil, courseID: "courseID") Verify(interactor, .voteThread(voted: .value(true), threadID: .value("1"))) @@ -131,7 +159,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteParentResponseSuccess() async throws { - + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -141,7 +178,7 @@ final class BaseResponsesViewModelTests: XCTestCase { viewModel.postComments?.voted = true - result = await viewModel.vote(id: "2", isThread: false, voted: false, index: nil) + result = await viewModel.vote(id: "2", isThread: false, voted: false, index: nil, courseID: "courseID") Verify(interactor, .voteResponse(voted: .value(false), responseID: .value("2"))) @@ -153,7 +190,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteNoInternetError() async throws { - + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -161,7 +207,7 @@ final class BaseResponsesViewModelTests: XCTestCase { Given(interactor, .voteThread(voted: .any, threadID: .any, willThrow: noInternetError)) - result = await viewModel.vote(id: "1", isThread: true, voted: true, index: 1) + result = await viewModel.vote(id: "1", isThread: true, voted: true, index: 1, courseID: "courseID") Verify(interactor, .voteThread(voted: .value(true), threadID: .value("1"))) @@ -172,13 +218,22 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteUnknownError() async throws { - + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false Given(interactor, .voteThread(voted: .any, threadID: .any, willThrow: NSError())) - result = await viewModel.vote(id: "1", isThread: true, voted: true, index: nil) + result = await viewModel.vote(id: "1", isThread: true, voted: true, index: nil, courseID: "courseID") Verify(interactor, .voteThread(voted: .value(true), threadID: .value("1"))) @@ -189,6 +244,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagThreadSuccess() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -196,7 +261,7 @@ final class BaseResponsesViewModelTests: XCTestCase { Given(interactor, .flagThread(abuseFlagged: .any, threadID: .any, willProduce: {_ in})) - result = await viewModel.flag(id: "1", isThread: true, abuseFlagged: true, index: nil) + result = await viewModel.flag(id: "1", isThread: true, abuseFlagged: true, index: nil, courseID: "courseID") Verify(interactor, .flagThread(abuseFlagged: .value(true), threadID: .value("1"))) @@ -208,6 +273,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagCommentSuccess() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -215,7 +290,7 @@ final class BaseResponsesViewModelTests: XCTestCase { Given(interactor, .flagComment(abuseFlagged: .any, commentID: .any, willProduce: {_ in})) - result = await viewModel.flag(id: "1", isThread: false, abuseFlagged: true, index: 0) + result = await viewModel.flag(id: "1", isThread: false, abuseFlagged: true, index: 0, courseID: "courseID") Verify(interactor, .flagComment(abuseFlagged: .value(true), commentID: .value("1"))) @@ -227,6 +302,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagNoInternetError() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -234,7 +319,7 @@ final class BaseResponsesViewModelTests: XCTestCase { Given(interactor, .flagThread(abuseFlagged: .any, threadID: .any, willThrow: noInternetError)) - result = await viewModel.flag(id: "1", isThread: true, abuseFlagged: true, index: 1) + result = await viewModel.flag(id: "1", isThread: true, abuseFlagged: true, index: 1, courseID: "courseID") Verify(interactor, .flagThread(abuseFlagged: .value(true), threadID: .value("1"))) @@ -245,12 +330,22 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagUnknownError() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false Given(interactor, .flagThread(abuseFlagged: .any, threadID: .any, willThrow: NSError())) - result = await viewModel.flag(id: "1", isThread: true, abuseFlagged: true, index: nil) + result = await viewModel.flag(id: "1", isThread: true, abuseFlagged: true, index: nil, courseID: "courseID") Verify(interactor, .flagThread(abuseFlagged: .value(true), threadID: .value("1"))) @@ -261,6 +356,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadSuccess() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -280,6 +385,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadNoInternetError() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -298,6 +413,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadUnknownError() async throws { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) var result = false @@ -314,6 +439,16 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testAddNewPost() { + let interactor = DiscussionInteractorProtocolMock() + let router = DiscussionRouterMock() + let config = ConfigMock() + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock(), + analytics: DiscussionAnalyticsMock() + ) viewModel.postComments = post diff --git a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift index 68efad3ea..9f997e04b 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift @@ -214,7 +214,8 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) Given(interactor, .getQuestionComments(threadID: .any, page: .any, @@ -244,7 +245,8 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) Given(interactor, .getDiscussionComments(threadID: .any, page: .any, @@ -274,7 +276,8 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -306,7 +309,8 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .readBody(threadID: .any, willThrow: NSError())) Given(interactor, .getQuestionComments(threadID: .any, page: .any, willThrow: NSError())) @@ -334,7 +338,8 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) let post = Post(authorName: "", authorAvatar: "", @@ -356,7 +361,7 @@ final class ThreadViewModelTests: XCTestCase { Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willReturn: post) ) - await viewModel.postComment(threadID: "1", rawBody: "1", parentID: nil) + await viewModel.postComment(courseID: "CourseID", threadID: "1", rawBody: "1", parentID: nil) Verify(interactor, .addCommentTo(threadID: .value("1"), rawBody: .value("1"), parentID: .value(nil))) @@ -375,13 +380,14 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: noInternetError) ) - await viewModel.postComment(threadID: "1", rawBody: "1", parentID: nil) + await viewModel.postComment(courseID: "CourseID", threadID: "1", rawBody: "1", parentID: nil) Verify(interactor, .addCommentTo(threadID: .value("1"), rawBody: .value("1"), parentID: .value(nil))) @@ -400,11 +406,12 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError()) ) - await viewModel.postComment(threadID: "1", rawBody: "1", parentID: nil) + await viewModel.postComment(courseID: "CourseID", threadID: "1", rawBody: "1", parentID: nil) Verify(interactor, .addCommentTo(threadID: .value("1"), rawBody: .value("1"), parentID: .value(nil))) @@ -424,7 +431,8 @@ final class ThreadViewModelTests: XCTestCase { router: router, config: config, storage: CoreStorageMock(), - postStateSubject: .init(.readed(id: "1"))) + postStateSubject: .init(.readed(id: "1")), + analytics: DiscussionAnalyticsMock()) viewModel.totalPages = 2 viewModel.comments = userComments + userComments diff --git a/Discussion/DiscussionTests/Presentation/CreateNewThread/CreateNewThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/CreateNewThread/CreateNewThreadViewModelTests.swift index f8fb82177..adaec8c4e 100644 --- a/Discussion/DiscussionTests/Presentation/CreateNewThread/CreateNewThreadViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/CreateNewThread/CreateNewThreadViewModelTests.swift @@ -30,7 +30,13 @@ final class CreateNewThreadViewModelTests: XCTestCase { let config = ConfigMock() var result = false - let viewModel = CreateNewThreadViewModel(interactor: interactor, router: router, config: config) + let viewModel = CreateNewThreadViewModel( + interactor: interactor, + router: router, + config: config, + analytics: DiscussionAnalyticsMock(), + storage: CoreStorageMock() + ) Given(interactor, .createNewThread(newThread: .any, willProduce: {_ in})) @@ -51,7 +57,13 @@ final class CreateNewThreadViewModelTests: XCTestCase { let config = ConfigMock() var result = false - let viewModel = CreateNewThreadViewModel(interactor: interactor, router: router, config: config) + let viewModel = CreateNewThreadViewModel( + interactor: interactor, + router: router, + config: config, + analytics: DiscussionAnalyticsMock(), + storage: CoreStorageMock() + ) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -74,7 +86,13 @@ final class CreateNewThreadViewModelTests: XCTestCase { let config = ConfigMock() var result = false - let viewModel = CreateNewThreadViewModel(interactor: interactor, router: router, config: config) + let viewModel = CreateNewThreadViewModel( + interactor: interactor, + router: router, + config: config, + analytics: DiscussionAnalyticsMock(), + storage: CoreStorageMock() + ) Given(interactor, .createNewThread(newThread: .any, willThrow: NSError())) diff --git a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift index 2aaba39ee..cab7add8b 100644 --- a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift @@ -106,11 +106,13 @@ final class ResponsesViewModelTests: XCTestCase { let config = ConfigMock() var result = false - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .getCommentResponses(commentID: .any, page: .any, willReturn: (userComments, Pagination(next: "", @@ -134,11 +136,13 @@ final class ResponsesViewModelTests: XCTestCase { let config = ConfigMock() var result = false - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -161,11 +165,13 @@ final class ResponsesViewModelTests: XCTestCase { let config = ConfigMock() var result = false - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .getCommentResponses(commentID: .any, page: .any, willThrow: NSError())) @@ -185,11 +191,13 @@ final class ResponsesViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willReturn: post)) @@ -207,11 +215,13 @@ final class ResponsesViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -231,11 +241,13 @@ final class ResponsesViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError())) @@ -253,11 +265,13 @@ final class ResponsesViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = ResponsesViewModel(interactor: interactor, + let viewModel = ResponsesViewModel(courseID: "courseID", + interactor: interactor, router: router, config: config, storage: CoreStorageMock(), - threadStateSubject: .init(.postAdded(id: "1"))) + threadStateSubject: .init(.postAdded(id: "1")), + analytics: DiscussionAnalyticsMock()) viewModel.totalPages = 2 viewModel.comments = userComments diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 80e519d62..ba9ed15c5 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -1253,7 +1253,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; CEBA52752CEBB69100619E2B /* XCRemoteSwiftPackageReference "openedx-app-firebase-analytics-ios" */ = { @@ -1261,7 +1261,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-firebase-analytics-ios"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 91bff664e..876026351 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -408,7 +408,9 @@ class ScreenAssembly: Assembly { blockID, courseID, router.currentCourseTabSelection - )! + )!, + appStorage: r.resolve(CoreStorage.self)!, + analytics: r.resolve(CourseAnalytics.self)! ) } ) @@ -437,7 +439,9 @@ class ScreenAssembly: Assembly { languages: languages, playerStateSubject: playerStateSubject, connectivity: r.resolve(ConnectivityProtocol.self)!, - playerHolder: holder + playerHolder: holder, + appStorage: r.resolve(CoreStorage.self)!, + analytics: r.resolve(CourseAnalytics.self)! ) } ) @@ -584,17 +588,20 @@ class ScreenAssembly: Assembly { router: r.resolve(DiscussionRouter.self)!, config: r.resolve(ConfigProtocol.self)!, storage: r.resolve(CoreStorage.self)!, - postStateSubject: subject + postStateSubject: subject, + analytics: r.resolve(DiscussionAnalytics.self)! ) } - container.register(ResponsesViewModel.self) { @MainActor r, subject in + container.register(ResponsesViewModel.self) { @MainActor r, subject, courseID in ResponsesViewModel( + courseID: courseID, interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(ConfigProtocol.self)!, storage: r.resolve(CoreStorage.self)!, - threadStateSubject: subject + threadStateSubject: subject, + analytics: r.resolve(DiscussionAnalytics.self)! ) } @@ -602,7 +609,9 @@ class ScreenAssembly: Assembly { CreateNewThreadViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(ConfigProtocol.self)! + config: r.resolve(ConfigProtocol.self)!, + analytics: r.resolve(DiscussionAnalytics.self)!, + storage: r.resolve(CoreStorage.self)! ) } diff --git a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift index 33b40bc3a..3f496db24 100644 --- a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift +++ b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift @@ -17,7 +17,7 @@ import WhatsNew import Swinject import OEXFoundation -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length class AnalyticsManager: AuthorizationAnalytics, MainScreenAnalytics, DiscoveryAnalytics, @@ -741,6 +741,94 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.bulkDeleteVideosSubsection, parameters: parameters) } + public func videoLoaded(courseID: String, blockID: String, videoURL: String) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.blockID: blockID, + EventParamKey.videoURL: videoURL, + EventParamKey.category: EventCategory.video, + EventParamKey.name: EventBIValue.videoLoaded.rawValue + ] + + logEvent(.videoLoaded, parameters: parameters) + } + + public func videoPlayed(courseID: String, blockID: String, videoURL: String) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.blockID: blockID, + EventParamKey.videoURL: videoURL, + EventParamKey.category: EventCategory.video, + EventParamKey.name: EventBIValue.videoPlayed.rawValue + ] + + logEvent(.videoPlayed, parameters: parameters) + } + + public func videoSpeedChange( + courseID: String, + blockID: String, + videoURL: String, + oldSpeed: Float, + newSpeed: Float, + currentTime: Double, + duration: Double + ) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.blockID: blockID, + EventParamKey.videoURL: videoURL, + EventParamKey.oldSpeed: oldSpeed, + EventParamKey.newSpeed: newSpeed, + EventParamKey.currentTime: currentTime, + EventParamKey.duration: duration, + EventParamKey.category: EventCategory.video, + EventParamKey.name: EventBIValue.videoSpeedChange.rawValue + ] + + logEvent(.videoSpeedChange, parameters: parameters) + } + + public func videoPaused( + courseID: String, + blockID: String, + videoURL: String, + currentTime: Double, + duration: Double + ) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.blockID: blockID, + EventParamKey.videoURL: videoURL, + EventParamKey.currentTime: currentTime, + EventParamKey.duration: duration, + EventParamKey.category: EventCategory.video, + EventParamKey.name: EventBIValue.videoPaused.rawValue + ] + + logEvent(.videoPaused, parameters: parameters) + } + + public func videoCompleted( + courseID: String, + blockID: String, + videoURL: String, + currentTime: Double, + duration: Double + ) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.blockID: blockID, + EventParamKey.videoURL: videoURL, + EventParamKey.currentTime: currentTime, + EventParamKey.duration: duration, + EventParamKey.category: EventCategory.video, + EventParamKey.name: EventBIValue.videoCompleted.rawValue + ] + + logEvent(.videoCompleted, parameters: parameters) + } + // MARK: Discussion public func discussionAllPostsClicked(courseId: String, courseName: String) { let parameters = [ @@ -771,6 +859,123 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.discussionTopicClicked, parameters: parameters) } + public func discussionCreateNewPost( + courseID: String, + topicID: String, + postType: String, + followPost: Bool, + author: String + ) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.topicID: topicID, + EventParamKey.postType: postType, + EventParamKey.followPost: followPost, + EventParamKey.author: author, + EventParamKey.name: EventBIValue.discussionPostCreated.rawValue + ] + logEvent(.discussionPostCreated, parameters: parameters) + } + + public func discussionResponseAdded( + courseID: String, + threadID: String, + responseID: String, + author: String + ) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.threadID: threadID, + EventParamKey.responseID: responseID, + EventParamKey.author: author, + EventParamKey.name: EventBIValue.discussionResponseAdded.rawValue + ] + logEvent(.discussionResponseAdded, parameters: parameters) + } + + public func discussionCommentAdded( + courseID: String, + threadID: String, + responseID: String, + commentID: String, + author: String + ) { + let parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.threadID: threadID, + EventParamKey.responseID: responseID, + EventParamKey.commentID: commentID, + EventParamKey.author: author, + EventParamKey.name: EventBIValue.discussionCommentAdded.rawValue + ] + logEvent(.discussionCommentAdded, parameters: parameters) + } + + public func discussionFollowToggle( + courseID: String, + threadID: String, + author: String, + follow: Bool + ) { + var parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.threadID: threadID, + EventParamKey.author: author, + EventParamKey.follow: follow, + EventParamKey.name: EventBIValue.discussionFollowToggle.rawValue + ] + + logEvent(.discussionFollowToggle, parameters: parameters) + } + + public func discussionLikeToggle( + courseID: String, + threadID: String, + responseID: String? = nil, + commentID: String? = nil, + author: String, + discussionType: String, + like: Bool + ) { + var parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.threadID: threadID, + EventParamKey.author: author, + EventParamKey.like: like, + EventParamKey.discussionType: discussionType, + EventParamKey.name: EventBIValue.discussionLikeToggle.rawValue + ] + + parameters.setObjectOrNil(responseID, forKey: EventParamKey.responseID) + parameters.setObjectOrNil(commentID, forKey: EventParamKey.commentID) + + logEvent(.discussionLikeToggle, parameters: parameters) + } + + public func discussionReportToggle( + courseID: String, + threadID: String, + responseID: String? = nil, + commentID: String? = nil, + author: String, + discussionType: String, + report: Bool + ) { + var parameters: [String: Any] = [ + EventParamKey.courseID: courseID, + EventParamKey.threadID: threadID, + EventParamKey.author: author, + EventParamKey.report: report, + EventParamKey.discussionType: discussionType, + EventParamKey.name: EventBIValue.discussionReportToggle.rawValue + ] + + parameters.setObjectOrNil(responseID, forKey: EventParamKey.responseID) + parameters.setObjectOrNil(commentID, forKey: EventParamKey.commentID) + + logEvent(.discussionReportToggle, parameters: parameters) + } + // MARK: app review public func appreview( @@ -826,4 +1031,4 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.whatnewClose, parameters: parameters) } } -// swiftlint:enable type_body_length +// swiftlint:enable type_body_length file_length diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift index 39ee78e18..a4de3ac8d 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift @@ -407,6 +407,7 @@ public class DeepLinkManager { !parentID.isEmpty, let parentComment = try? await self.discussionInteractor.getResponse(responseID: parentID) { router.showComment( + courseID: courseDetails.courseID, comment: comment, parentComment: parentComment.post, isBlackedOut: isBlackedOut @@ -443,6 +444,7 @@ public class DeepLinkManager { !commentParentID.isEmpty, let parentComment = try? await self.discussionInteractor.getResponse(responseID: commentParentID) { router.showComment( + courseID: courseDetails.courseID, comment: comment, parentComment: parentComment.post, isBlackedOut: isBlackedOut diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index 5601b3db7..661f82207 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -49,6 +49,7 @@ public protocol DeepLinkRouter: BaseRouter { isBlackedOut: Bool ) func showComment( + courseID: String, comment: UserComment, parentComment: Post, isBlackedOut: Bool @@ -235,11 +236,13 @@ extension Router: DeepLinkRouter { } public func showComment( + courseID: String, comment: UserComment, parentComment: Post, isBlackedOut: Bool ) { showComments( + courseID: courseID, commentID: comment.commentID, parentComment: parentComment, threadStateSubject: .init(.none), @@ -370,6 +373,7 @@ public class DeepLinkRouterMock: BaseRouterMock, DeepLinkRouter { isBlackedOut: Bool ) {} public func showComment( + courseID: String, comment: UserComment, parentComment: Post, isBlackedOut: Bool diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 4cb43429c..326b2b8e1 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -674,6 +674,7 @@ public class Router: AuthorizationRouter, } public func showComments( + courseID: String, commentID: String, parentComment: Post, threadStateSubject: CurrentValueSubject, @@ -681,7 +682,7 @@ public class Router: AuthorizationRouter, animated: Bool ) { let router = Container.shared.resolve(DiscussionRouter.self)! - let viewModel = Container.shared.resolve(ResponsesViewModel.self, argument: threadStateSubject)! + let viewModel = Container.shared.resolve(ResponsesViewModel.self, arguments: threadStateSubject, courseID)! viewModel.isBlackedOut = isBlackedOut let view = ResponsesView( commentID: commentID, diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 063020b3b..fb2969b20 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -1748,7 +1748,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; CEBCA42F2CC13CB900076589 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */ = { diff --git a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift index 481dd2a15..54a3c7ff5 100644 --- a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift +++ b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift @@ -159,9 +159,16 @@ struct ProfileBottomSheet: View { .fill(type.bgColor()) ) .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(type.frameColor()) + Theme.Shapes.buttonShape + .stroke(style: .init( + lineWidth: 1, + lineCap: .round, + lineJoin: .round, + miterLimit: 1) + ) + .foregroundColor( + type.frameColor() + ) ) } } diff --git a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj index dbf013488..114c9c097 100644 --- a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj +++ b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj @@ -1532,7 +1532,7 @@ repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { kind = exactVersion; - version = 1.0.1; + version = 1.0.2; }; }; /* End XCRemoteSwiftPackageReference section */