Moves OnlineChecker to a singleton class with cache.

This commit is contained in:
Vitor Pamplona
2023-06-24 15:53:22 -04:00
parent 775094356c
commit b040e0ca13
6 changed files with 226 additions and 75 deletions
@@ -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<String, OnlineCheckResult>(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
}
}
}
@@ -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<Note>() {
@@ -41,7 +38,7 @@ class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter<No
.asSequence()
.filter { it ->
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<No
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
}
fun checkIfOnline(url: String?): Boolean {
if (url.isNullOrBlank()) return false
val request = Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
return try {
HttpClient.getHttpClient().newCall(request).execute().code == 200
} catch (e: Exception) {
Log.e("LiveActivities", "Failed to check streaming url $url", e)
false
}
}
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.note
import android.content.Intent
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -92,6 +93,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
@@ -143,7 +145,9 @@ import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DiviserThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
@@ -2611,64 +2615,110 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewMo
@Composable
fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> 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<List<Pair<Participant, User>>>(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<ImmutableList<Pair<Participant, User>>>(
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
)
}
}
}
@@ -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
@@ -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())
}
}
+8
View File
@@ -456,4 +456,12 @@
<string name="minimum_pow">Minimum PoW</string>
<string name="auth">Auth</string>
<string name="payment">Payment</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">ENDED</string>
<string name="live_stream_planned_tag">SCHEDULED</string>
<string name="live_stream_is_offline">Livestream is Offline</string>
<string name="live_stream_has_ended">Livestream Ended</string>
</resources>