Skip to content

Commit

Permalink
[Podcast Update Feed] Add Tooltip (#3561)
Browse files Browse the repository at this point in the history
  • Loading branch information
mebarbosa authored Feb 7, 2025
1 parent cefa571 commit 4d67661
Show file tree
Hide file tree
Showing 13 changed files with 271 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,10 @@ class MainActivity :
return binding.snackbarFragment
}

override fun setFullScreenDarkOverlayViewVisibility(visible: Boolean) {
binding.fullScreenDarkOverlayView.isVisible = visible
}

override fun onMiniPlayerHidden() {
updateSnackbarPosition(miniPlayerOpen = false)
settings.updateBottomInset(0)
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />

<View
android:id="@+id/fullScreenDarkOverlayView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000"
android:visibility="gone" />

<au.com.shiftyjelly.pocketcasts.views.component.RadioactiveLineView
android:id="@+id/radioactiveLineView"
android:layout_width="match_parent"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class AutomotiveSettingsActivity : AppCompatActivity(), FragmentHostListener {
return findViewById(android.R.id.content)
}

override fun setFullScreenDarkOverlayViewVisibility(visible: Boolean) {
}

override fun showAccountUpgradeNow(autoSelectPlus: Boolean) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,10 +446,10 @@ class PodcastAdapter(
}
}

fun setPodcast(podcast: Podcast) {
fun setPodcast(podcast: Podcast, forceHeaderExpanded: Boolean? = null) {
// expand the podcast description and details if the user hasn't subscribed
if (this.podcast.uuid != podcast.uuid) {
headerExpanded = !podcast.isSubscribed
headerExpanded = forceHeaderExpanded ?: !podcast.isSubscribed
ratingsViewModel.loadRatings(podcast.uuid)
ratingsViewModel.refreshPodcastRatings(podcast.uuid)
onHeaderSummaryToggled(headerExpanded, false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.core.view.isGone
Expand All @@ -23,6 +31,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsEvent
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsTracker
import au.com.shiftyjelly.pocketcasts.analytics.SourceView
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.localization.extensions.getStringPlural
import au.com.shiftyjelly.pocketcasts.models.entity.BaseEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.Bookmark
Expand Down Expand Up @@ -90,6 +99,7 @@ import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.asObservable
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -168,6 +178,9 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener {

private var listState: Parcelable? = null

private var tooltipOffset by mutableStateOf(IntOffset.Zero)
private var canShowTooltip by mutableStateOf(false)

private var currentSnackBar: Snackbar? = null

private val onScrollListener = object : RecyclerView.OnScrollListener() {
Expand Down Expand Up @@ -737,6 +750,39 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener {
super.onViewCreated(view, savedInstanceState)
binding?.setupMultiSelect()

binding?.composeTooltipHost?.setContent {
AppTheme(theme.activeTheme) {
val shouldShowPodcastTooltip by viewModel.shouldShowPodcastTooltip.collectAsState()

var show by remember { mutableStateOf(true) }

LaunchedEffect(canShowTooltip, shouldShowPodcastTooltip) {
show = canShowTooltip && shouldShowPodcastTooltip && FeatureFlag.isEnabled(Feature.PODCAST_FEED_UPDATE)
}

if (show) {
PodcastTooltip(
title = stringResource(LR.string.podcast_feed_update_tooltip_title),
subtitle = stringResource(LR.string.podcast_feed_update_tooltip_subtitle),
offset = tooltipOffset,
onTooltipShown = {
(activity as? FragmentHostListener)?.setFullScreenDarkOverlayViewVisibility(true)
analyticsTracker.track(AnalyticsEvent.PODCAST_REFRESH_EPISODE_TOOLTIP_SHOWN)
},
onDismissRequest = {
hideTooltip()
},
onCloseButtonClick = {
analyticsTracker.track(AnalyticsEvent.PODCAST_REFRESH_EPISODE_TOOLTIP_DISMISSED)
hideTooltip()
},
)
} else {
(activity as? FragmentHostListener)?.setFullScreenDarkOverlayViewVisibility(false)
}
}
}

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.multiSelectBookmarksHelper.navigationState
Expand All @@ -750,6 +796,46 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener {
}
}

private fun configureTooltip() {
lifecycleScope.launch {
delay(1.seconds) // Delay to wait the recyclerview to be configured

val headerPositionInList = 2 // See: au.com.shiftyjelly.pocketcasts.podcasts.view.podcast.PodcastAdapter.setEpisodes

val viewHolder = binding?.episodesRecyclerView?.findViewHolderForAdapterPosition(headerPositionInList)
as? PodcastAdapter.EpisodeHeaderViewHolder

val anchorView = viewHolder?.binding?.btnEpisodeOptions
anchorView?.let { showTooltipAbove(it) }
}
}

private fun showTooltipAbove(view: View) {
val anchorLocation = IntArray(2)
view.getLocationOnScreen(anchorLocation)

val composeLocation = IntArray(2)
val tooltipComposeView = binding?.composeTooltipHost ?: return

tooltipComposeView.getLocationOnScreen(composeLocation)

val anchorX = anchorLocation[0] - composeLocation[0] + (view.width / 2)
var anchorY = anchorLocation[1] - composeLocation[1] - 360

if (anchorY < 0) {
anchorY = 0
}

tooltipOffset = IntOffset(anchorX, anchorY)
canShowTooltip = true
}

private fun hideTooltip() {
(activity as? FragmentHostListener)?.setFullScreenDarkOverlayViewVisibility(false)
viewModel.hidePodcastRefreshTooltip()
canShowTooltip = false
}

private fun onShareBookmarkClick() {
lifecycleScope.launch {
val (podcast, episode, bookmark) = viewModel.getSharedBookmark() ?: return@launch
Expand Down Expand Up @@ -867,7 +953,8 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener {
binding?.toolbar?.setBackgroundColor(backgroundColor)
binding?.headerBackgroundPlaceholder?.setBackgroundColor(backgroundColor)

adapter?.setPodcast(podcast)
val forceHeaderExpanded = !viewModel.shouldShowPodcastTooltip.value && FeatureFlag.isEnabled(Feature.PODCAST_FEED_UPDATE)
adapter?.setPodcast(podcast, forceHeaderExpanded = forceHeaderExpanded)

viewModel.archiveEpisodeLimit()

Expand Down Expand Up @@ -903,6 +990,7 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener {
podcast = state.podcast,
context = requireContext(),
)
configureTooltip()
}
PodcastTab.BOOKMARKS -> {
adapter?.setBookmarks(
Expand Down Expand Up @@ -1005,6 +1093,7 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener {
binding = null
currentSnackBar?.dismiss()
currentSnackBar = null
(activity as? FragmentHostListener)?.setFullScreenDarkOverlayViewVisibility(false)
}

private fun archiveAllPlayed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package au.com.shiftyjelly.pocketcasts.podcasts.view.podcast

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import au.com.shiftyjelly.pocketcasts.compose.CallOnce
import au.com.shiftyjelly.pocketcasts.compose.components.TextH30
import au.com.shiftyjelly.pocketcasts.compose.components.TextP40
import au.com.shiftyjelly.pocketcasts.compose.theme
import au.com.shiftyjelly.pocketcasts.localization.R as LR

@Composable
fun PodcastTooltip(
title: String,
subtitle: String,
offset: IntOffset,
onTooltipShown: () -> Unit,
onDismissRequest: () -> Unit,
onCloseButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val tooltipColor = MaterialTheme.theme.colors.primaryUi01

CallOnce {
onTooltipShown.invoke()
}

Popup(
alignment = Alignment.TopStart,
offset = offset,
onDismissRequest = onDismissRequest,
) {
Box(
modifier = modifier.background(Color.Transparent).padding(16.dp).widthIn(max = 400.dp),
) {
TooltipContent(tooltipColor, title, subtitle, onCloseButtonClick)

TooltipArrow(tooltipColor)
}
}
}

@Composable
private fun TooltipContent(
tooltipColor: Color,
title: String,
subtitle: String,
onCloseButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
backgroundColor = tooltipColor,
shape = RoundedCornerShape(8.dp),
elevation = 0.dp,
modifier = modifier,
) {
Row(
modifier = Modifier.background(tooltipColor).padding(16.dp),
verticalAlignment = Alignment.Top,
) {
Column(
modifier = Modifier.weight(1f),
) {
TextH30(
text = title,
modifier = Modifier.padding(bottom = 4.dp),
)

TextP40(
text = subtitle,
fontSize = 12.sp,
color = MaterialTheme.theme.colors.primaryText02,
)
}

Spacer(modifier = Modifier.width(12.dp))

Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(LR.string.close),
tint = MaterialTheme.theme.colors.primaryIcon02,
modifier = Modifier
.align(Alignment.Top)
.width(24.dp)
.clickable {
onCloseButtonClick.invoke()
},
)
}
}
}

@Composable
private fun BoxScope.TooltipArrow(tooltipColor: Color) {
Canvas(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 28.dp),
) {
val triangleSize = 12.dp.toPx()

drawPath(
path = Path().apply {
moveTo(0f, -2f)
lineTo(triangleSize, 0f)
lineTo(triangleSize / 2f, triangleSize)
close()
},
color = tooltipColor,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -102,6 +103,8 @@ class PodcastViewModel
private val _refreshState = MutableSharedFlow<RefreshState>()
val refreshState = _refreshState.asSharedFlow()

val shouldShowPodcastTooltip = MutableStateFlow(settings.showPodcastRefreshTooltip.value)

val groupedEpisodes: MutableLiveData<List<List<PodcastEpisode>>> = MutableLiveData()
val signInState = userManager.getSignInState().toLiveData()

Expand Down Expand Up @@ -592,6 +595,11 @@ class PodcastViewModel
}
}

fun hidePodcastRefreshTooltip() {
settings.showPodcastRefreshTooltip.set(false, updateModifiedAt = false)
shouldShowPodcastTooltip.value = false
}

private fun trackEpisodeBulkEvent(event: AnalyticsEvent, count: Int) {
episodeAnalytics.trackBulkEvent(
event,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />

<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeTooltipHost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

</FrameLayout>

</LinearLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ enum class AnalyticsEvent(val key: String) {
PODCAST_SCREEN_REFRESH_EPISODE_LIST("podcast_screen_refresh_episode_list"),
PODCAST_SCREEN_REFRESH_NEW_EPISODE_FOUND("podcast_screen_refresh_new_episode_found"),
PODCAST_SCREEN_REFRESH_NO_EPISODES_FOUND("podcast_screen_refresh_no_episodes_found"),
PODCAST_REFRESH_EPISODE_TOOLTIP_SHOWN("podcast_refresh_episode_tooltip_shown"),
PODCAST_REFRESH_EPISODE_TOOLTIP_DISMISSED("podcast_refresh_episode_tooltip_dismissed"),

/* Podcast Settings */
PODCAST_SETTINGS_FEED_ERROR_TAPPED("podcast_settings_feed_error_tapped"),
Expand Down
2 changes: 2 additions & 0 deletions modules/services/localization/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,8 @@
<string name="podcast_group_unplayed" translatable="false">@string/unplayed</string>
<string name="podcast_hide_archived">Hide archived</string>
<string name="podcast_load_error">There was an error loading the podcast.</string>
<string name="podcast_feed_update_tooltip_title">Fresh episodes, coming right up!</string>
<string name="podcast_feed_update_tooltip_subtitle">Pull down or use this menu to see if there\'s something new.</string>
<string name="podcast_loading">Loading your podcasts</string>
<string name="podcast_next_episode_any_day_now">Next episode any day now</string>
<string name="podcast_next_episode_today">Next episode today</string>
Expand Down
Loading

0 comments on commit 4d67661

Please sign in to comment.