Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

End of Year: Share text #570

Merged
merged 5 commits into from
Nov 16, 2022
Merged
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
@@ -0,0 +1,125 @@
package au.com.shiftyjelly.pocketcasts.endofyear

import android.content.Context
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.Story
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryListenedCategories
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryListenedNumbers
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryListeningTime
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryLongestEpisode
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryTopFivePodcasts
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryTopPodcast
import au.com.shiftyjelly.pocketcasts.servers.list.ListServerManager
import au.com.shiftyjelly.pocketcasts.settings.stats.StatsHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
import au.com.shiftyjelly.pocketcasts.localization.R as LR

class ShareableTextProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val listServerManager: ListServerManager,
) {
private var shortURL: String = Settings.SERVER_SHORT_URL
private val hashtags = listOf("pocketcasts", "endofyear2022").joinToString(" ") { "#$it" }

suspend fun getShareableDataForStory(
story: Story,
): ShareTextData {
var showShortURLAtEnd = false
val shareableLink: String = when (story) {
is StoryTopFivePodcasts -> {
try {
listServerManager.createPodcastList(
title = context.resources.getString(
LR.string.end_of_year_story_top_podcasts_share_text,
""
),
description = "",
podcasts = story.topPodcasts.map { it.toPodcast() }
) ?: shortURL
} catch (ex: Exception) {
Timber.e(ex)
shortURL
}
Comment on lines +32 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

This api call failed for me with a 403 everytime I tried to share the top 5 podcasts, so I fell through to the generic PC link each time. Not sure what is causing that. I tried both logged-in and logged-out on the staging and prod server with the same result each time. πŸ€”

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you confirm if you can share podcasts and episodes in general (from podcasts or episode fragments)?

I'll merge this PR for now. Based on your response, I'll recheck what might be going wrong with this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you confirm if you can share podcasts and episodes in general

Good idea. I'm also getting an error when I try to create a shared list of podcasts from the podcasts page, so I don't think this is an issue with your EOY work (and fwiw, I can share a list with our current production builds, so I must be doing something weird locally).

}
is StoryTopPodcast -> {
"$shortURL/podcast/${story.topPodcast.uuid}"
}
is StoryLongestEpisode -> {
"$shortURL/episode/${story.longestEpisode.uuid}"
}
else -> {
showShortURLAtEnd = true
shortURL
}
}
val textWithLink = getTextWithLink(story, shareableLink)
return ShareTextData(
textWithLink = textWithLink,
hashTags = hashtags,
showShortURLAtEnd = showShortURLAtEnd
)
}

private fun getTextWithLink(
story: Story,
shareableLink: String
): String {
val resources = context.resources
return when (story) {
is StoryListeningTime -> {
val timeText =
StatsHelper.secondsToFriendlyString(story.listeningTimeInSecs, resources)
resources.getString(
LR.string.end_of_year_story_listened_to_share_text,
timeText
)
}

is StoryListenedCategories -> {
resources.getString(
LR.string.end_of_year_story_listened_to_categories_share_text,
story.listenedCategories.size
)
}

is StoryListenedNumbers -> {
resources.getString(
LR.string.end_of_year_story_listened_to_numbers_share_text,
story.listenedNumbers.numberOfPodcasts,
story.listenedNumbers.numberOfEpisodes
)
}

is StoryTopPodcast -> {
resources.getString(
LR.string.end_of_year_story_top_podcast_share_text,
shareableLink
)
}

is StoryTopFivePodcasts -> {
resources.getString(
LR.string.end_of_year_story_top_podcasts_share_text,
shareableLink
)
}

is StoryLongestEpisode -> {
resources.getString(
LR.string.end_of_year_story_longest_episode_share_text,
shareableLink
)
}

else -> ""
}
}

data class ShareTextData(
val textWithLink: String,
val hashTags: String,
val showShortURLAtEnd: Boolean
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import au.com.shiftyjelly.pocketcasts.compose.bars.NavigationButton
import au.com.shiftyjelly.pocketcasts.compose.buttons.RowOutlinedButton
import au.com.shiftyjelly.pocketcasts.compose.components.TextP50
import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider
import au.com.shiftyjelly.pocketcasts.endofyear.ShareableTextProvider.ShareTextData
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State
import au.com.shiftyjelly.pocketcasts.endofyear.views.SegmentedProgressIndicator
import au.com.shiftyjelly.pocketcasts.endofyear.views.convertibleToBitmap
Expand All @@ -69,6 +70,7 @@ import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryTopFivePodcas
import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryTopListenedCategoriesView
import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryTopPodcastView
import au.com.shiftyjelly.pocketcasts.models.db.helper.ListenedNumbers
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.Story
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryEpilogue
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryIntro
Expand Down Expand Up @@ -126,21 +128,29 @@ fun StoriesPage(
) {
Box(modifier = modifier.size(dialogSize)) {
when (state) {
is State.Loaded -> StoriesView(
state = state as State.Loaded,
progress = viewModel.progress,
onSkipPrevious = { viewModel.skipPrevious() },
onSkipNext = { viewModel.skipNext() },
onPause = { viewModel.pause() },
onStart = { viewModel.start() },
onCloseClicked = onCloseClicked,
onReplayClicked = { viewModel.replay() },
onShareClicked = {
viewModel.onShareClicked(it, context) { file ->
showShareForFile(context, file, shareLauncher)
}
},
)
is State.Loaded -> {
StoriesView(
state = state as State.Loaded,
progress = viewModel.progress,
onSkipPrevious = { viewModel.skipPrevious() },
onSkipNext = { viewModel.skipNext() },
onPause = { viewModel.pause() },
onStart = { viewModel.start() },
onCloseClicked = onCloseClicked,
onReplayClicked = { viewModel.replay() },
onShareClicked = {
val currentStory = requireNotNull((state as State.Loaded).currentStory)
viewModel.onShareClicked(it, currentStory, context) { file, shareTextData ->
showShareForFile(
context,
file,
shareLauncher,
shareTextData
)
}
},
)
}
State.Loading -> StoriesLoadingView(onCloseClicked)
State.Error -> StoriesErrorView(onCloseClicked)
}
Expand Down Expand Up @@ -193,6 +203,9 @@ private fun StoriesView(
.padding(8.dp)
.fillMaxWidth(),
)
if (state.preparingShareText) {
LoadingOverContentView()
}
CloseButtonView(onCloseClicked)
}
}
Expand All @@ -204,6 +217,13 @@ private fun StoriesView(
}
}

@Composable
private fun LoadingOverContentView() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(color = Color.White)
}
}

@Composable
private fun StorySharableContent(
story: Story,
Expand Down Expand Up @@ -383,13 +403,19 @@ private fun showShareForFile(
context: Context,
file: File,
shareLauncher: ActivityResultLauncher<Intent>,
shareTextData: ShareTextData,
) {
try {
val uri = FileUtil.getUriForFile(context, file)
var shareText = "${shareTextData.textWithLink} ${shareTextData.hashTags}"
if (shareTextData.showShortURLAtEnd) {
shareText += " ${Settings.SERVER_SHORT_URL}"
}

val chooserIntent = ShareCompat.IntentBuilder(context)
.setType("image/png")
.addStream(uri)
.setText(shareText)
.setChooserTitle(LR.string.end_of_year_share_via)
.createChooserIntent()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.graphics.Bitmap
import androidx.annotation.FloatRange
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.endofyear.ShareableTextProvider.ShareTextData
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State.Loaded.SegmentsData
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.EndOfYearManager
import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.Story
Expand All @@ -25,6 +26,7 @@ import kotlin.math.roundToInt
class StoriesViewModel @Inject constructor(
private val endOfYearManager: EndOfYearManager,
private val fileUtilWrapper: FileUtilWrapper,
private val shareableTextProvider: ShareableTextProvider,
) : ViewModel() {

private val mutableState = MutableStateFlow<State>(State.Loading)
Expand Down Expand Up @@ -128,8 +130,9 @@ class StoriesViewModel @Inject constructor(

fun onShareClicked(
onCaptureBitmap: () -> Bitmap,
story: Story,
context: Context,
showShareForFile: (File) -> Unit,
showShareForFile: (File, ShareTextData) -> Unit,
) {
pause()
viewModelScope.launch {
Expand All @@ -139,7 +142,14 @@ class StoriesViewModel @Inject constructor(
EOY_STORY_SAVE_FOLDER_NAME,
EOY_STORY_SAVE_FILE_NAME
)
savedFile?.let { showShareForFile.invoke(it) }

val currentState = (state.value as State.Loaded)
mutableState.value = currentState.copy(preparingShareText = true)

val shareTextData = shareableTextProvider.getShareableDataForStory(story)
mutableState.value = currentState.copy(preparingShareText = false)
Comment on lines +146 to +150
Copy link
Contributor

@mchowning mchowning Nov 14, 2022

Choose a reason for hiding this comment

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

I wonder if there's any way the state could get mutated while we're waiting for the suspended shareableTextProvicer.getShareableDataForStory call? It doesn't seem likely, but I was able to get a bit of weird behavior by quickly tapping on Story after tapping the Share button (it would jump back to the beginning of the Story sequence). That's probably unrelated to this, but figured I'd mention it.

device-2022-11-14-112608.mp4

Copy link
Contributor Author

@ashiagr ashiagr Nov 15, 2022

Choose a reason for hiding this comment

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

This is because StoriesViewModel initialized inside the StoriesPage dialog composable is not scoped to it but to the activity/ fragment that contains it. As a workaround, I manually call viewmodel's clear() and start() which doesn't seem to be working well.

This SO answer helped me understand it:

Compose does not offer any mechanism to scope ViewModels to an individual @composable - any ViewModels you create outside of a NavHost's destination is scoped to the activity/fragment that contains your ComposeView/where you call setContent and, thus, lives for the entire lifetime of your Compose hierarchy - that's why you always get the same instance back.

Feature request: https://issuetracker.google.com/issues/165642391

I'll open another PR with a fix or temporary workaround.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think that disabling the UI as soon as the Share button is touched might help?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I switched to DialogFragment with composable content in #581. I'll shortly make it ready for review and we can test it again if it fixes the issue.


savedFile?.let { showShareForFile.invoke(it, shareTextData) }
}
}

Expand Down Expand Up @@ -183,6 +193,7 @@ class StoriesViewModel @Inject constructor(
data class Loaded(
val currentStory: Story?,
val segmentsData: SegmentsData,
val preparingShareText: Boolean = false,
) : State() {
data class SegmentsData(
val widths: List<Float> = emptyList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewAspectRatioForTablet
import au.com.shiftyjelly.pocketcasts.utils.Util
import au.com.shiftyjelly.pocketcasts.utils.extensions.deviceAspectRatio
import au.com.shiftyjelly.pocketcasts.utils.extensions.dpToPx

/* Returns a callback to get bitmap for the passed composable.
The composable is converted to ComposeView and laid out into AndroidView otherwise an illegal state exception is thrown:
Expand All @@ -37,10 +38,12 @@ fun convertibleToBitmap(
)

return {
val height = composeView.width * getAspectRatioForBitmap(context)
val availableHeight = height - 50.dpToPx(context) // Reduce approx share button height
createBitmapFromView(
view = composeView,
width = composeView.width,
height = (composeView.width * getAspectRatioForBitmap(context)).toInt()
height = availableHeight.toInt()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ class StoriesViewModelTest {
whenever(endOfYearManager.loadStories()).thenReturn(flowOf(mockStories))
return StoriesViewModel(
endOfYearManager = endOfYearManager,
fileUtilWrapper = fileUtilWrapper
fileUtilWrapper = fileUtilWrapper,
shareableTextProvider = mock()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ abstract class EpisodeDao {

@Query(
"""
SELECT episodes.title, episodes.duration, podcasts.uuid as podcastUuid, podcasts.title as podcastTitle, podcasts.primary_color as tintColorForLightBg, podcasts.secondary_color as tintColorForDarkBg
SELECT episodes.uuid, episodes.title, episodes.duration, podcasts.uuid as podcastUuid, podcasts.title as podcastTitle, podcasts.primary_color as tintColorForLightBg, podcasts.secondary_color as tintColorForDarkBg
FROM episodes
JOIN podcasts ON episodes.podcast_id = podcasts.uuid
WHERE episodes.last_playback_interaction_date IS NOT NULL AND episodes.last_playback_interaction_date > :fromEpochMs AND episodes.last_playback_interaction_date < :toEpochMs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package au.com.shiftyjelly.pocketcasts.models.db.helper
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast

data class LongestEpisode(
val uuid: String,
val title: String,
val duration: Double,
val podcastUuid: String,
Expand Down