Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ struct DiscoverVideoEpisodeCell: View {
.focused($focusedButton, equals: FocusValues.playEpisode)
.setFocus(section: DiscoverType.video.rawValue)
.contextMenu {
DiscoveryEpisodeMenuButtons(podcastUuid: model.episode.podcastUuid ?? "", episodeUuid: model.episode.uuid ?? "", showNotesEpisode: $showNotesEpisode)
DiscoveryEpisodeMenuButtons(podcastUuid: model.episode.podcastUuid ?? "", episodeUuid: model.episode.uuid ?? "", source: AnalyticsSource(rawValue: source) ?? .unknown, showNotesEpisode: $showNotesEpisode)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SergioEstevao , is that what you are thinking in terms of action sources? How does this PR look?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good! You can move it for review. Thanks for doing this!

}
Spacer()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ListeningHistoryViewModel {

nonisolated private func loadEpisodeViewModels(using dataManager: DataManager) -> [EpisodeRowViewModel] {
dataManager.fetchHistoryEpisodes().map { episode in
EpisodeRowViewModel(episode: episode, podcast: episode.parentPodcast(dataManager: dataManager))
EpisodeRowViewModel(episode: episode, podcast: episode.parentPodcast(dataManager: dataManager), analyticsSource: .listeningHistory)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Pocket Casts TV App/UI/Home/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class HomeViewModel {
guard let latest: Episode = dataManager.findLatestEpisode(podcast: podcast) else {
continue
}
let result = EpisodeRowViewModel(episode: latest, podcast: podcast)
let result = EpisodeRowViewModel(episode: latest, podcast: podcast, analyticsSource: .home)
newEpisodes.append(result)
}

Expand All @@ -65,7 +65,7 @@ class HomeViewModel {

private func makeRowViewModel(for episode: BaseEpisode) -> EpisodeRowViewModel {
let podcast = (episode as? Episode).flatMap { $0.parentPodcast(dataManager: dataManager) }
return EpisodeRowViewModel(episode: episode, podcast: podcast)
return EpisodeRowViewModel(episode: episode, podcast: podcast, analyticsSource: .home)
}

private func observeDataChanges() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ struct PlaylistDetailView: View {
List {
Section {
ForEach(model.episodes, id: \.uuid) { episode in
EpisodeRowWithActions(model: EpisodeRowViewModel(episode: episode, podcast: nil), focus: $rowFocus)
EpisodeRowWithActions(model: EpisodeRowViewModel(episode: episode, podcast: nil, analyticsSource: .filters), focus: $rowFocus)
.prefersDefaultFocus(episode.uuid == model.episodes.first?.uuid, in: episodeListNamespace)
.listRowInsets(Layout.rowInsets)
}
Expand Down
12 changes: 7 additions & 5 deletions Pocket Casts TV App/UI/Podcasts/EpisodeRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,15 @@ struct DiscoveryEpisodeMenuButtons: View {

let podcastUuid: String
let episodeUuid: String
var source: AnalyticsSource = .unknown
@Binding var showNotesEpisode: DiscoveryLoadedEpisode?

@Environment(\.requireAccount) private var requireAccount

var body: some View {
Button(L10n.tvEpisodeShowNotesAction) { load { showNotesEpisode = $0 } }
Button(L10n.playNextInUpNext) { requireAccount { load { EpisodeUpNextActions.playNext($0.episode) } } }
Button(L10n.playLastInUpNext) { requireAccount { load { EpisodeUpNextActions.playLast($0.episode) } } }
Button(L10n.playNextInUpNext) { requireAccount { load { EpisodeUpNextActions.playNext($0.episode, source: source) } } }
Button(L10n.playLastInUpNext) { requireAccount { load { EpisodeUpNextActions.playLast($0.episode, source: source) } } }
}

private func load(_ action: @escaping (DiscoveryLoadedEpisode) -> Void) {
Expand All @@ -306,13 +307,14 @@ private struct DiscoveryEpisodeContextMenuModifier: ViewModifier {

let podcastUuid: String
let episodeUuid: String
var source: AnalyticsSource = .unknown

@State private var showNotesEpisode: DiscoveryLoadedEpisode?

func body(content: Content) -> some View {
content
.contextMenu {
DiscoveryEpisodeMenuButtons(podcastUuid: podcastUuid, episodeUuid: episodeUuid, showNotesEpisode: $showNotesEpisode)
DiscoveryEpisodeMenuButtons(podcastUuid: podcastUuid, episodeUuid: episodeUuid, source: source, showNotesEpisode: $showNotesEpisode)
}
.sheet(item: $showNotesEpisode) { episode in
EpisodeShowNotesView(episode: episode.episode, podcast: episode.podcast)
Expand All @@ -321,8 +323,8 @@ private struct DiscoveryEpisodeContextMenuModifier: ViewModifier {
}

extension View {
func discoveryEpisodeContextMenu(podcastUuid: String, episodeUuid: String) -> some View {
modifier(DiscoveryEpisodeContextMenuModifier(podcastUuid: podcastUuid, episodeUuid: episodeUuid))
func discoveryEpisodeContextMenu(podcastUuid: String, episodeUuid: String, source: AnalyticsSource = .unknown) -> some View {
modifier(DiscoveryEpisodeContextMenuModifier(podcastUuid: podcastUuid, episodeUuid: episodeUuid, source: source))
}
}

Expand Down
30 changes: 23 additions & 7 deletions Pocket Casts TV App/UI/Podcasts/EpisodeRowViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ class EpisodeRowViewModel: Identifiable {
var progress: Double
var id: String { episode.uuid }

/// The analytics `source` for actions taken on this row, set by the screen that
/// builds it (Podcast, Up Next, Starred, …). This is the tvOS equivalent of iOS's
/// per-screen `AnalyticsSourceProvider`: actions push it onto the shared analytics
/// helpers so the events `EpisodeManager`/`PlaybackManager` fire carry the source.
let analyticsSource: AnalyticsSource

private var cancellables: Set<AnyCancellable> = []
private let playbackManager: PlaybackManager

init(episode: BaseEpisode, podcast: Podcast?, playbackManager: PlaybackManager = PlaybackManager.shared) {
init(episode: BaseEpisode, podcast: Podcast?, analyticsSource: AnalyticsSource = .unknown, playbackManager: PlaybackManager = PlaybackManager.shared) {
self.episode = episode
self.podcast = podcast
self.analyticsSource = analyticsSource
self.playbackManager = playbackManager
self.progress = episode.playedUpTo / episode.duration
setupObservers()
Expand Down Expand Up @@ -83,14 +90,15 @@ class EpisodeRowViewModel: Identifiable {
}

func playNext() {
EpisodeUpNextActions.playNext(episode, playbackManager: playbackManager)
EpisodeUpNextActions.playNext(episode, source: analyticsSource, playbackManager: playbackManager)
}

func playLast() {
EpisodeUpNextActions.playLast(episode, playbackManager: playbackManager)
EpisodeUpNextActions.playLast(episode, source: analyticsSource, playbackManager: playbackManager)
}

func markAsPlayed() {
AnalyticsEpisodeHelper.shared.currentSource = analyticsSource
EpisodeManager.markAsPlayed(episode: episode, fireNotification: true)
ToastManager.shared.show(L10n.tvEpisodeMarkedAsPlayed)
}
Expand All @@ -109,17 +117,20 @@ class EpisodeRowViewModel: Identifiable {

func archive() {
guard let episode = episode as? Episode else { return }
AnalyticsEpisodeHelper.shared.currentSource = analyticsSource
EpisodeManager.archiveEpisode(episode: episode, fireNotification: true)
ToastManager.shared.show(L10n.tvEpisodeArchived)
}

func unarchive() {
guard let episode = episode as? Episode else { return }
AnalyticsEpisodeHelper.shared.currentSource = analyticsSource
EpisodeManager.unarchiveEpisode(episode: episode, fireNotification: true)
ToastManager.shared.show(L10n.tvEpisodeUnarchived)
}

func removeFromUpNext() {
AnalyticsEpisodeHelper.shared.currentSource = analyticsSource
playbackManager.removeIfPlayingOrQueued(episode: episode, fireNotification: true, userInitiated: true)
ToastManager.shared.show(L10n.tvEpisodeRemovedFromUpNext)
}
Expand Down Expand Up @@ -160,22 +171,27 @@ class EpisodeRowViewModel: Identifiable {
/// Up Next queue actions shared by the episode view model and the lazily-loaded
/// discovery/search context menus, so the move-vs-add queue logic lives in one place.
enum EpisodeUpNextActions {
static func playNext(_ episode: BaseEpisode, playbackManager: PlaybackManager = .shared) {
static func playNext(_ episode: BaseEpisode, source: AnalyticsSource = .unknown, playbackManager: PlaybackManager = .shared) {
if playbackManager.inUpNext(episode: episode) {
playbackManager.queue.move(episode: episode, to: 0)
Analytics.track(.upNextQueueReordered, properties: ["direction": "up", "is_next": true])
Analytics.track(.upNextQueueReordered, properties: ["direction": "up", "is_next": true, "source": source])
} else {
// `addToUpNext` fires `episodeAddedToUpNext` through the shared helper, which
// consumes `currentSource`; set it only on this branch so it can't leak into a
// later event when we just reorder an episode that's already queued.
AnalyticsEpisodeHelper.shared.currentSource = source
playbackManager.addToUpNext(episode: episode, ignoringQueueLimit: true, toTop: true, userInitiated: true)
}
ToastManager.shared.show(L10n.tvEpisodeWillPlayNext)
}

static func playLast(_ episode: BaseEpisode, playbackManager: PlaybackManager = .shared) {
static func playLast(_ episode: BaseEpisode, source: AnalyticsSource = .unknown, playbackManager: PlaybackManager = .shared) {
if playbackManager.inUpNext(episode: episode) {
let queueCount = playbackManager.queue.upNextCount()
playbackManager.queue.move(episode: episode, to: max(queueCount - 1, 0))
Analytics.track(.upNextQueueReordered, properties: ["direction": "down", "is_next": queueCount == 1])
Analytics.track(.upNextQueueReordered, properties: ["direction": "down", "is_next": queueCount == 1, "source": source])
} else {
AnalyticsEpisodeHelper.shared.currentSource = source
playbackManager.addToUpNext(episode: episode, ignoringQueueLimit: true, toTop: false, userInitiated: true)
}
ToastManager.shared.show(L10n.tvEpisodeWillPlayLast)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class PodcastDetailViewModel {
return
}
let allEpisodes = dataManager.fetchEpisodes(podcast: podcast, includeArchived: showArchived).map {
EpisodeRowViewModel(episode: $0, podcast: podcast)
EpisodeRowViewModel(episode: $0, podcast: podcast, analyticsSource: .podcastScreen)
}
await MainActor.run {
self.podcast = podcast
Expand Down
2 changes: 1 addition & 1 deletion Pocket Casts TV App/UI/Search/SearchResultsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ struct SearchResultsView<ViewModel: SearchableViewModel>: View {
SearchEpisodeRow(model: episode)
}
.buttonStyle(.card)
.discoveryEpisodeContextMenu(podcastUuid: episode.podcastUuid, episodeUuid: episode.uuid)
.discoveryEpisodeContextMenu(podcastUuid: episode.podcastUuid, episodeUuid: episode.uuid, source: .search)
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class StarredEpisodesViewModel {

nonisolated private func loadEpisodeViewModels(using dataManager: DataManager) -> [EpisodeRowViewModel] {
dataManager.fetchStarredEpisodes().map { episode in
EpisodeRowViewModel(episode: episode, podcast: episode.parentPodcast(dataManager: dataManager))
EpisodeRowViewModel(episode: episode, podcast: episode.parentPodcast(dataManager: dataManager), analyticsSource: .starred)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Pocket Casts TV App/UI/UpNext/UpNextViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class UpNextViewModel {
private func loadEpisodeViewModels(using dataManager: DataManager) -> [EpisodeRowViewModel] {
dataManager.allUpNextEpisodes().dropFirst().map { episode in
let podcast = (episode as? Episode).flatMap { $0.parentPodcast(dataManager: dataManager) }
return EpisodeRowViewModel(episode: episode, podcast: podcast)
return EpisodeRowViewModel(episode: episode, podcast: podcast, analyticsSource: .upNext)
}
}

Expand Down
4 changes: 4 additions & 0 deletions podcasts/Analytics/Helpers/AnalyticsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ enum AnalyticsSource: String, AnalyticsDescribable {
case userSatisfactionSurvey = "user_satisfaction_survey"
case recommendations
case playlistEditor = "playlist_editor"
/// tvOS: episode actions performed from the Home tab and Search screens, which
/// have no iOS equivalent. Raw values match `DiscoverAnalytics.homeSource`/`searchSource`.
case home
case search
case unknown

var analyticsDescription: String { rawValue }
Expand Down