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 @@ -8,6 +8,7 @@ import au.com.shiftyjelly.pocketcasts.models.entity.BaseEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
import au.com.shiftyjelly.pocketcasts.models.entity.PodcastEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.UserEpisode
import au.com.shiftyjelly.pocketcasts.models.type.SignInState
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfItem
import au.com.shiftyjelly.pocketcasts.repositories.chromecast.ChromeCastAnalytics
Expand All @@ -19,6 +20,7 @@ import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.UserEpisodeManager
import au.com.shiftyjelly.pocketcasts.repositories.transcript.TranscriptManager
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.utils.featureflag.Feature
Expand Down Expand Up @@ -46,7 +48,8 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.asFlow
import kotlinx.coroutines.reactive.asFlow as asReactiveFlow
import kotlinx.coroutines.rx2.asFlow as asRxFlow

@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
Expand All @@ -60,6 +63,7 @@ class ShelfSharedViewModel @Inject constructor(
private val settings: Settings,
private val userEpisodeManager: UserEpisodeManager,
private val transcriptManager: TranscriptManager,
private val userManager: UserManager,
private val downloadQueue: DownloadQueue,
) : ViewModel() {
private val upNextStateObservable: Observable<UpNextQueue.State> =
Expand Down Expand Up @@ -102,11 +106,12 @@ class ShelfSharedViewModel @Inject constructor(

val uiState = combine(
settings.shelfItems.flow,
shelfUpNextObservable.asFlow(),
shelfUpNextObservable.asFlow()
shelfUpNextObservable.asRxFlow(),
shelfUpNextObservable.asRxFlow()
.mapNotNull { state -> (state as? UpNextQueue.State.Loaded)?.episode?.uuid }
.flatMapLatest { episodeUuid -> transcriptManager.observeIsTranscriptAvailable(episodeUuid) },
videoStateFlow,
userManager.getSignInState().asReactiveFlow(),
::createUiState,
).stateIn(
viewModelScope,
Expand All @@ -119,6 +124,7 @@ class ShelfSharedViewModel @Inject constructor(
shelfUpNext: UpNextQueue.State,
isTranscriptAvailable: Boolean,
videoState: VideoState,
signInState: SignInState,
): UiState {
val episode = (shelfUpNext as? UpNextQueue.State.Loaded)?.episode
val streamHasVideo = videoState.streamVideoState == StreamVideoState.HasVideo || videoState.streamVideoState == StreamVideoState.Unknown
Expand All @@ -129,7 +135,12 @@ class ShelfSharedViewModel @Inject constructor(
return uiState.value.copy(
shelfItems = shelfItems.filter { it.showIf(episode) && (it != ShelfItem.StreamSelector || canToggleVideo) },
episode = episode,
isTranscriptAvailable = isTranscriptAvailable,
isTranscriptAvailable = isTranscriptAvailable ||
(
episode is PodcastEpisode &&
signInState.isSignedInAsPlusOrPatron &&
FeatureFlag.isEnabled(Feature.ON_DEMAND_TRANSCRIPTS)
),
isVideoRenderingEnabled = videoState.renderingEnabled && streamHasVideo,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package au.com.shiftyjelly.pocketcasts.player.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.models.entity.BaseEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.PodcastEpisode
import au.com.shiftyjelly.pocketcasts.player.viewmodel.ShelfSharedViewModel.Companion.MIN_SHELF_ITEMS_SIZE
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfItem
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfRowItem
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfTitle
import au.com.shiftyjelly.pocketcasts.repositories.transcript.TranscriptManager
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.utils.featureflag.Feature
import au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag
import com.automattic.eventhorizon.EventHorizon
import com.automattic.eventhorizon.PlayerShelfOverflowMenuRearrangeActionMovedEvent
import com.automattic.eventhorizon.PlayerShelfOverflowMenuRearrangeFinishedEvent
Expand All @@ -22,9 +26,11 @@ import java.util.Collections
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow
import timber.log.Timber
import au.com.shiftyjelly.pocketcasts.localization.R as LR

Expand All @@ -33,18 +39,28 @@ class ShelfViewModel @AssistedInject constructor(
@Assisted private val episodeId: String,
@Assisted private val isEditable: Boolean,
private val transcriptManager: TranscriptManager,
private val userManager: UserManager,
private val eventHorizon: EventHorizon,
private val settings: Settings,
) : ViewModel() {
private var _uiState: MutableStateFlow<UiState> = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState
private var hasTranscriptRow = false
private var isEligiblePaidListener = false

init {
viewModelScope.launch {
transcriptManager.observeIsTranscriptAvailable(episodeId)
combine(
transcriptManager.observeIsTranscriptAvailable(episodeId),
userManager.getSignInState().asFlow(),
) { isAvailable, signInState ->
isAvailable to signInState.isSignedInAsPlusOrPatron
}
.stateIn(viewModelScope)
.collectLatest { isAvailable ->
_uiState.update { it.copy(isTranscriptAvailable = isAvailable) }
.collectLatest { (isAvailable, isPaid) ->
hasTranscriptRow = isAvailable
isEligiblePaidListener = isPaid
updateTranscriptAvailability()
}
}
}
Expand All @@ -70,10 +86,26 @@ class ShelfViewModel @AssistedInject constructor(
items
},
episode = episode,
isTranscriptAvailable = isTranscriptAvailableFor(episode),
)
}
}

private fun updateTranscriptAvailability() {
_uiState.update { state ->
state.copy(
isTranscriptAvailable = isTranscriptAvailableFor(state.episode),
)
}
}
Comment thread
stefanosala marked this conversation as resolved.

private fun isTranscriptAvailableFor(episode: BaseEpisode?) = hasTranscriptRow ||
(
episode is PodcastEpisode &&
isEligiblePaidListener &&
FeatureFlag.isEnabled(Feature.ON_DEMAND_TRANSCRIPTS)
)

fun onShelfItemMove(
fromPosition: Int,
toPosition: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import au.com.shiftyjelly.pocketcasts.models.entity.PodcastEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.UserEpisode
import au.com.shiftyjelly.pocketcasts.models.to.Chapter
import au.com.shiftyjelly.pocketcasts.models.to.Chapters
import au.com.shiftyjelly.pocketcasts.models.type.SignInState
import au.com.shiftyjelly.pocketcasts.models.type.Subscription
import au.com.shiftyjelly.pocketcasts.models.type.SubscriptionPlatform
import au.com.shiftyjelly.pocketcasts.payment.BillingCycle
Expand All @@ -28,6 +29,7 @@ import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.UserEpisodeManager
import au.com.shiftyjelly.pocketcasts.repositories.transcript.TranscriptManager
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
import au.com.shiftyjelly.pocketcasts.sharedtest.InMemoryFeatureFlagRule
import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule
Expand All @@ -41,7 +43,9 @@ import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.rx2.asFlowable
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
Expand Down Expand Up @@ -86,6 +90,9 @@ class ShelfSharedViewModelTest {
@Mock
private lateinit var settings: Settings

@Mock
private lateinit var userManager: UserManager

@Mock
private lateinit var upNextQueue: UpNextQueue

Expand Down Expand Up @@ -188,6 +195,28 @@ class ShelfSharedViewModelTest {
}
}

@Test
fun `paid listener can open missing podcast transcript when feature is enabled`() = runTest {
FeatureFlag.setEnabled(Feature.ON_DEMAND_TRANSCRIPTS, true)
val episode = PodcastEpisode("uuid", publishedDate = Date())
initViewModel(subscription = plusSubscription, currentEpisode = episode)

val state = shelfSharedViewModel.uiState.first { it.episode != null }

assertTrue(state.isTranscriptAvailable)
}

@Test
fun `free listener cannot open missing podcast transcript`() = runTest {
FeatureFlag.setEnabled(Feature.ON_DEMAND_TRANSCRIPTS, true)
val episode = PodcastEpisode("uuid", publishedDate = Date())
initViewModel(subscription = null, currentEpisode = episode)

val state = shelfSharedViewModel.uiState.first { it.episode != null }

assertFalse(state.isTranscriptAvailable)
}

@Test
fun `when download button clicked, then episode download started snackbar message is shown`() = runTest {
initViewModel()
Expand Down Expand Up @@ -450,6 +479,9 @@ class ShelfSharedViewModelTest {
val userSubscriptionSetting = mock<UserSetting<Subscription?>>()
whenever(userSubscriptionSetting.value).thenReturn(subscription)
whenever(settings.cachedSubscription).thenReturn(userSubscriptionSetting)
whenever(userManager.getSignInState()).thenReturn(
flowOf<SignInState>(SignInState.SignedIn("email", subscription)).asFlowable(),
)

whenever(playbackManager.streamVideoState).thenReturn(MutableStateFlow(streamVideoState))
whenever(playbackManager.streamHlsAvailable).thenReturn(MutableStateFlow(hlsAvailable))
Expand All @@ -469,6 +501,7 @@ class ShelfSharedViewModelTest {
settings = settings,
userEpisodeManager = userEpisodeManager,
transcriptManager = transcriptManager,
userManager = userManager,
downloadQueue = mock(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import au.com.shiftyjelly.pocketcasts.analytics.testing.TestEventSink
import au.com.shiftyjelly.pocketcasts.models.converter.SafeDate
import au.com.shiftyjelly.pocketcasts.models.entity.PodcastEpisode
import au.com.shiftyjelly.pocketcasts.models.type.SignInState
import au.com.shiftyjelly.pocketcasts.player.viewmodel.ShelfViewModel.Companion.ERROR_MINIMUM_SHELF_ITEMS
import au.com.shiftyjelly.pocketcasts.player.viewmodel.ShelfViewModel.Companion.ERROR_SHELF_ITEM_INVALID_MOVE_POSITION
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.preferences.UserSetting
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfItem
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfRowItem
import au.com.shiftyjelly.pocketcasts.repositories.transcript.TranscriptManager
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule
import com.automattic.eventhorizon.EventHorizon
import com.automattic.eventhorizon.PlayerShelfOverflowMenuRearrangeActionMovedEvent
import com.automattic.eventhorizon.ShelfActionSourceType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.rx2.asFlowable
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
Expand All @@ -42,6 +45,9 @@ class ShelfViewModelTest {
@Mock
private lateinit var settings: Settings

@Mock
private lateinit var userManager: UserManager

private val eventSink = TestEventSink()

private lateinit var shelfViewModel: ShelfViewModel
Expand Down Expand Up @@ -215,11 +221,13 @@ class ShelfViewModelTest {
whenever(transcriptManager.observeIsTranscriptAvailable(episodeId)).thenReturn(flowOf(true))
val userSetting = mock<UserSetting<List<ShelfItem>>>()
whenever(settings.shelfItems).thenReturn(userSetting)
whenever(userManager.getSignInState()).thenReturn(flowOf<SignInState>(SignInState.SignedOut).asFlowable())

shelfViewModel = ShelfViewModel(
episodeId = episodeId,
isEditable = isEditable,
transcriptManager = transcriptManager,
userManager = userManager,
eventHorizon = EventHorizon(eventSink),
settings = settings,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,12 @@ class EpisodeFragment : BaseFragment() {
}
}

val tabs = buildMergedTabs(transcript, summaryText, hasChapters)
val tabs = buildMergedTabs(
transcript = transcript,
summaryText = summaryText,
hasChapters = hasChapters,
canRequestOnDemandTranscript = pageState.canRequestOnDemandTranscript,
)

val isSelectedTabAvailable = tabs.any { it.labelResId == selectedTab.labelResId }
LaunchedEffect(isSelectedTabAvailable) {
Expand Down Expand Up @@ -1080,7 +1085,12 @@ class EpisodeFragment : BaseFragment() {
if (isSummaryEnabled) {
val chaptersState = chaptersViewModel.uiState.collectAsState().value
val hasChapters = chaptersState.chaptersCount > 0
val tabs = buildMergedTabs(transcript, summaryText, hasChapters)
val tabs = buildMergedTabs(
transcript = transcript,
summaryText = summaryText,
hasChapters = hasChapters,
canRequestOnDemandTranscript = pageState.canRequestOnDemandTranscript,
)
val askTheEpisodeVisible = FeatureFlag.isEnabled(Feature.EPISODE_CHAT) && transcript != null

Column(modifier = Modifier.fillMaxWidth()) {
Expand Down Expand Up @@ -1177,6 +1187,7 @@ class EpisodeFragment : BaseFragment() {
transcript: Transcript.Text?,
summaryText: String?,
hasChapters: Boolean,
canRequestOnDemandTranscript: Boolean,
): List<ButtonTab> {
val tabClickHandlers = mapOf<Int, () -> Unit>(
LR.string.details to { viewModel.selectContentTab(EpisodeContentTab.DESCRIPTION) },
Expand All @@ -1186,20 +1197,24 @@ class EpisodeFragment : BaseFragment() {
},
LR.string.bookmarks to { viewModel.selectContentTab(EpisodeContentTab.BOOKMARKS) },
LR.string.transcript to {
if (transcript != null) {
if (transcript != null || canRequestOnDemandTranscript) {
viewModel.selectContentTab(EpisodeContentTab.TRANSCRIPT)
eventHorizon.track(
EpisodeDetailTranscriptCardTappedEvent(
episodeUuid = transcript.episodeUuid,
podcastUuid = transcript.podcastUuid ?: AnalyticsTracker.INVALID_OR_NULL_VALUE,
episodeUuid = transcript?.episodeUuid
?: viewModel.episode?.uuid
?: AnalyticsTracker.INVALID_OR_NULL_VALUE,
podcastUuid = transcript?.podcastUuid
?: viewModel.podcast?.uuid
?: AnalyticsTracker.INVALID_OR_NULL_VALUE,
Comment thread
stefanosala marked this conversation as resolved.
),
)
}
},
LR.string.summary to { viewModel.selectContentTab(EpisodeContentTab.SUMMARY) },
)
return mergedTabLabelResIds(
hasTranscript = transcript != null,
hasTranscript = transcript != null || canRequestOnDemandTranscript,
hasSummary = summaryText != null,
hasChapters = hasChapters,
).map { labelResId ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ class EpisodeFragmentViewModel @Inject constructor(
val episodePublishedDate: Date? = null,
val episodeDurationMs: Long? = null,
) {
val canRequestOnDemandTranscript
get() = isPlusUser && FeatureFlag.isEnabled(Feature.ON_DEMAND_TRANSCRIPTS)

internal fun selectContentTab(tab: EpisodeContentTab): EpisodePageState {
val contentTab = when (tab) {
EpisodeContentTab.DESCRIPTION -> EpisodeContentTab.DESCRIPTION
Expand All @@ -138,7 +141,11 @@ class EpisodeFragmentViewModel @Inject constructor(
}

internal fun withTranscript(transcript: Transcript?): EpisodePageState {
val contentTab = if (transcript == null && selectedContentTab == EpisodeContentTab.TRANSCRIPT) {
val contentTab = if (
transcript == null &&
selectedContentTab == EpisodeContentTab.TRANSCRIPT &&
!canRequestOnDemandTranscript
) {
EpisodeContentTab.DESCRIPTION
} else {
selectedContentTab
Expand Down
1 change: 1 addition & 0 deletions modules/features/transcripts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies {
implementation(libs.coroutines.reactive)
implementation(libs.coroutines.rx2)
implementation(libs.fragment.ktx)
implementation(libs.lifecycle.runtime.compose)
implementation(libs.rx2.java)

implementation(projects.modules.features.settings)
Expand Down
Loading