From b040e0ca13ab81ef54712324d6e223fccb5c053e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 24 Jun 2023 15:53:22 -0400 Subject: [PATCH] Moves OnlineChecker to a singleton class with cache. --- .../amethyst/service/OnlineCheck.kt | 40 +++++ .../ui/dal/HomeLiveActivitiesFeedFilter.kt | 24 +-- .../amethyst/ui/note/NoteCompose.kt | 170 +++++++++++++----- .../ui/screen/loggedIn/ChannelScreen.kt | 49 ++++- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 10 +- app/src/main/res/values/strings.xml | 8 + 6 files changed, 226 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt new file mode 100644 index 000000000..c93460035 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -0,0 +1,40 @@ +package com.vitorpamplona.amethyst.service + +import android.util.Log +import android.util.LruCache +import com.google.errorprone.annotations.Immutable +import com.vitorpamplona.amethyst.BuildConfig +import okhttp3.Request + +@Immutable +data class OnlineCheckResult(val timeInMs: Long, val online: Boolean) + +object OnlineChecker { + val checkOnlineCache = LruCache(10) + val fiveMinutes = 1000 * 60 * 5 + + fun isOnline(url: String?): Boolean { + checkNotInMainThread() + + if (url.isNullOrBlank()) return false + if ((checkOnlineCache.get(url)?.timeInMs ?: 0) > System.currentTimeMillis() - fiveMinutes) { + return checkOnlineCache.get(url).online + } + + val request = Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url) + .get() + .build() + + return try { + val result = HttpClient.getHttpClient().newCall(request).execute().code == 200 + checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result)) + result + } catch (e: Exception) { + checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false)) + Log.e("LiveActivities", "Failed to check streaming url $url", e) + false + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeLiveActivitiesFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeLiveActivitiesFeedFilter.kt index 532343efe..a0d665584 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeLiveActivitiesFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeLiveActivitiesFeedFilter.kt @@ -1,14 +1,11 @@ package com.vitorpamplona.amethyst.ui.dal -import android.util.Log -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent -import okhttp3.Request import java.util.Date class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter() { @@ -41,7 +38,7 @@ class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter val noteEvent = it.event - (noteEvent is LiveActivitiesEvent && noteEvent.createdAt > twoHrs && noteEvent.status() == "live" && checkIfOnline(noteEvent.streaming())) && + (noteEvent is LiveActivitiesEvent && noteEvent.createdAt > twoHrs && noteEvent.status() == "live" && OnlineChecker.isOnline(noteEvent.streaming())) && (it.author?.pubkeyHex in followingKeySet || (noteEvent.isTaggedHashes(followingTagSet))) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true @@ -55,20 +52,3 @@ class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter Unit) { + Row(modifier = Modifier.padding(top = 5.dp)) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + RenderLiveActivityEventInner(baseNote = baseNote, accountViewModel, nav) + } + } +} + +@Composable +fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return - val media = remember { noteEvent.streaming() } - val cover = remember { noteEvent.image() } - val subject = remember { noteEvent.title() } - val content = remember { noteEvent.summary() } - val participants = remember { noteEvent.participants() } + val eventUpdates by baseNote.live().metadata.observeAsState() - var participantUsers by remember { mutableStateOf>>(emptyList()) } + val media = remember(eventUpdates) { noteEvent.streaming() } + val cover = remember(eventUpdates) { noteEvent.image() } + val subject = remember(eventUpdates) { noteEvent.title() } + val content = remember(eventUpdates) { noteEvent.summary() } + val participants = remember(eventUpdates) { noteEvent.participants() } + val status = remember(eventUpdates) { noteEvent.status() } - LaunchedEffect(key1 = participants) { + var isOnline by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = media) { launch(Dispatchers.IO) { - participantUsers = participants.mapNotNull { part -> - LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } + isOnline = OnlineChecker.isOnline(media) + } + } + + Row( + verticalAlignment = CenterVertically, + modifier = Modifier + .padding(vertical = 5.dp) + .fillMaxWidth() + ) { + subject?.let { + Text( + text = it, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + + Crossfade(targetState = status) { + when (it) { + "live" -> { + if (isOnline) { + LiveFlag() + } + } + "planned" -> { + ScheduledFlag() + } } } } - Row(modifier = Modifier.padding(top = 5.dp)) { - Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Row() { - subject?.let { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)) { - Text( - text = it, - fontWeight = FontWeight.Bold, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth() - ) - } - } - } + var participantUsers by remember { + mutableStateOf>>( + persistentListOf() + ) + } - participantUsers.forEach { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(top = 5.dp, start = 10.dp, end = 10.dp) - .clickable { - nav("User/${it.second.pubkeyHex}") - } - ) { - ClickableUserPicture(it.second, 25.dp, accountViewModel) - Spacer(Modifier.width(5.dp)) - UsernameDisplay(it.second, Modifier.weight(1f)) - Spacer(Modifier.width(5.dp)) - it.first.role?.let { - Text( - text = it.capitalize(Locale.ROOT), - color = MaterialTheme.colors.placeholderText, - maxLines = 1 - ) - } - } - } + LaunchedEffect(key1 = eventUpdates) { + launch(Dispatchers.IO) { + val newParticipantUsers = participants.mapNotNull { part -> + LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } + }.toImmutableList() - media?.let { media -> + if (!equalImmutableLists(newParticipantUsers, participantUsers)) { + participantUsers = newParticipantUsers + } + } + } + + participantUsers.forEach { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(top = 5.dp, start = 10.dp, end = 10.dp) + .clickable { + nav("User/${it.second.pubkeyHex}") + } + ) { + ClickableUserPicture(it.second, 25.dp, accountViewModel) + Spacer(Modifier.width(5.dp)) + UsernameDisplay(it.second, Modifier.weight(1f)) + Spacer(Modifier.width(5.dp)) + it.first.role?.let { + Text( + text = it.capitalize(Locale.ROOT), + color = MaterialTheme.colors.placeholderText, + maxLines = 1 + ) + } + } + } + + media?.let { media -> + if (status == "live") { + if (isOnline) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp) @@ -2678,6 +2728,32 @@ fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, description = subject ) } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(10.dp) + .height(100.dp) + ) { + Text( + text = stringResource(id = R.string.live_stream_is_offline), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.Bold + ) + } + } + } else if (status == "ended") { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(10.dp) + .height(100.dp) + ) { + Text( + text = stringResource(id = R.string.live_stream_has_ended), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.Bold + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 7f4f717fb..c6c1ec034 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -714,9 +714,9 @@ private fun LiveChannelActionOptions( } @Composable -private fun LiveFlag() { +fun LiveFlag() { Text( - text = "LIVE", + text = stringResource(id = R.string.live_stream_live_tag), color = Color.White, fontWeight = FontWeight.Bold, modifier = remember { @@ -728,6 +728,51 @@ private fun LiveFlag() { ) } +@Composable +fun EndedFlag() { + Text( + text = stringResource(id = R.string.live_stream_ended_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = remember { + Modifier + .clip(SmallBorder) + .drawBehind { drawRect(Color.Black) } + .padding(horizontal = 5.dp) + } + ) +} + +@Composable +fun OfflineFlag() { + Text( + text = stringResource(id = R.string.live_stream_offline_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = remember { + Modifier + .clip(SmallBorder) + .drawBehind { drawRect(Color.Black) } + .padding(horizontal = 5.dp) + } + ) +} + +@Composable +fun ScheduledFlag() { + Text( + text = stringResource(id = R.string.live_stream_planned_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = remember { + Modifier + .clip(SmallBorder) + .drawBehind { drawRect(Color.Black) } + .padding(horizontal = 5.dp) + } + ) +} + @Composable private fun NoteCopyButton( note: Channel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 27afa7fdf..35f930804 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -36,8 +36,8 @@ import androidx.lifecycle.LifecycleEventObserver import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrHomeDataSource +import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent -import com.vitorpamplona.amethyst.ui.dal.checkIfOnline import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog import com.vitorpamplona.amethyst.ui.screen.FeedState @@ -76,6 +76,8 @@ fun HomeScreen( val lifeCycleOwner = LocalLifecycleOwner.current DisposableEffect(accountViewModel) { + liveActivitiesViewModel.invalidateData(true) + val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrHomeDataSource.invalidateFilters() @@ -193,7 +195,7 @@ private fun FeedLoaded( state = listState ) { itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> - CheckIfOnline(item) { + CheckIfLiveActivityIsOnline(item) { ChannelHeader( channelHex = remember { item.idHex }, showVideo = false, @@ -210,13 +212,13 @@ private fun FeedLoaded( } @Composable -fun CheckIfOnline(note: Note, whenOnline: @Composable () -> Unit) { +fun CheckIfLiveActivityIsOnline(note: Note, whenOnline: @Composable () -> Unit) { val noteState by note.live().metadata.observeAsState() var online by remember { mutableStateOf(false) } LaunchedEffect(key1 = noteState) { launch(Dispatchers.IO) { - online = checkIfOnline((note.event as? LiveActivitiesEvent)?.streaming()) + online = OnlineChecker.isOnline((note.event as? LiveActivitiesEvent)?.streaming()) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8739c4f29..6cbc6420e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,4 +456,12 @@ Minimum PoW Auth Payment + + LIVE + OFFLINE + ENDED + SCHEDULED + + Livestream is Offline + Livestream Ended