From 5796052f36cd0c380f9c2f950376df7be722362c Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 15 Sep 2025 18:21:34 +0200 Subject: [PATCH 1/3] added live/offline status indicator for the DisplayLiveBubbles method Checks streaming URLs for LiveActivitiesChannel and relay URLs for both channel types --- .../loggedIn/home/live/LiveStatusIndicator.kt | 100 ++++++++++++++++++ .../home/live/RenderEphemeralBubble.kt | 7 ++ .../home/live/RenderLiveActivityBubble.kt | 7 ++ 3 files changed, 114 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt new file mode 100644 index 000000000..6aad8f356 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.service.OnlineChecker +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.delay + +@Composable +fun LiveStatusIndicator( + isOnline: Boolean, + modifier: Modifier = Modifier, +) { + Canvas( + modifier = modifier.size(8.dp), + ) { + drawCircle( + color = if (isOnline) Color.Red else Color.Black, + radius = size.minDimension / 2, + ) + } +} + +@Composable +fun LiveStatusIndicatorForChannel( + channel: Channel, + accountViewModel: AccountViewModel, + modifier: Modifier = Modifier, +) { + var isOnline by remember { mutableStateOf(false) } + + LaunchedEffect(channel) { + while (true) { + isOnline = checkChannelIsOnline(channel, accountViewModel) + delay(30_000) // Check every 30 seconds + } + } + + LiveStatusIndicator(isOnline = isOnline, modifier = modifier) +} + +private suspend fun checkChannelIsOnline( + channel: Channel, + accountViewModel: AccountViewModel, +): Boolean = + when (channel) { + is LiveActivitiesChannel -> { + // Check if streaming URL is online, fall back to relay check + val streamingUrl = channel.info?.streaming() + if (!streamingUrl.isNullOrBlank()) { + accountViewModel.checkVideoIsOnline(streamingUrl) + } else { + // Check relay connection + val relayUrl = channel.relayHintUrl() + if (relayUrl != null) { + OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) + } else { + false + } + } + } + is EphemeralChatChannel -> { + // Check relay connection for ephemeral chat + val relayUrl = channel.roomId.relayUrl + OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) + } + else -> false + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt index e56e00ac3..4a7886545 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel @@ -48,6 +49,12 @@ fun RenderEphemeralBubble( nav.nav { routeFor(channel) } }, ) { + LiveStatusIndicatorForChannel( + channel = channel, + accountViewModel = accountViewModel, + modifier = Modifier.align(Alignment.CenterVertically), + ) + Spacer(StdHorzSpacer) RenderUsers(channel, accountViewModel, nav) Spacer(StdHorzSpacer) Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt index b2f0b7bda..7e85e3504 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel @@ -48,6 +49,12 @@ fun RenderLiveActivityBubble( nav.nav { routeFor(channel) } }, ) { + LiveStatusIndicatorForChannel( + channel = channel, + accountViewModel = accountViewModel, + modifier = Modifier.align(Alignment.CenterVertically), + ) + Spacer(StdHorzSpacer) RenderUsers(channel, accountViewModel, nav) Spacer(StdHorzSpacer) Text( From f4501d799d2fb3c35c9fa8c0fe1fc8d911634a84 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 15 Sep 2025 18:25:52 +0200 Subject: [PATCH 2/3] Added withContext(Dispatchers.IO) for network call --- .../loggedIn/home/live/LiveStatusIndicator.kt | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 6aad8f356..9cd42712c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live +import android.util.Log import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable @@ -36,7 +37,9 @@ import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext @Composable fun LiveStatusIndicator( @@ -75,26 +78,34 @@ private suspend fun checkChannelIsOnline( channel: Channel, accountViewModel: AccountViewModel, ): Boolean = - when (channel) { - is LiveActivitiesChannel -> { - // Check if streaming URL is online, fall back to relay check - val streamingUrl = channel.info?.streaming() - if (!streamingUrl.isNullOrBlank()) { - accountViewModel.checkVideoIsOnline(streamingUrl) - } else { - // Check relay connection - val relayUrl = channel.relayHintUrl() - if (relayUrl != null) { - OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) - } else { - false + withContext(Dispatchers.IO) { + try { + when (channel) { + is LiveActivitiesChannel -> { + // Check if streaming URL is online, fall back to relay check + val streamingUrl = channel.info?.streaming() + if (!streamingUrl.isNullOrBlank()) { + accountViewModel.checkVideoIsOnline(streamingUrl) + } else { + // Check relay connection + val relayUrl = channel.relayHintUrl() + if (relayUrl != null) { + OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) + } else { + false + } + } } + is EphemeralChatChannel -> { + // Check relay connection for ephemeral chat + val relayUrl = channel.roomId.relayUrl + OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) + } + else -> false } + } catch (e: Exception) { + Log.d("LiveStatusIndicator", "Network error checking channel ${channel.toBestDisplayName()}: ${e.message}") + // Return false if any network error occurs + false } - is EphemeralChatChannel -> { - // Check relay connection for ephemeral chat - val relayUrl = channel.roomId.relayUrl - OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) - } - else -> false } From 1d0f584c3e6e12ea79d9a7615a9614ad3e79c179 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 22 Sep 2025 13:58:48 +0200 Subject: [PATCH 3/3] use produceState instead of LaunchedEffect --- .../loggedIn/home/live/LiveStatusIndicator.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 9cd42712c..8a9c2966f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -24,11 +24,8 @@ import android.util.Log import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -62,16 +59,17 @@ fun LiveStatusIndicatorForChannel( accountViewModel: AccountViewModel, modifier: Modifier = Modifier, ) { - var isOnline by remember { mutableStateOf(false) } - - LaunchedEffect(channel) { + val isOnline by produceState(initialValue = false, key1 = channel) { while (true) { - isOnline = checkChannelIsOnline(channel, accountViewModel) - delay(30_000) // Check every 30 seconds + value = checkChannelIsOnline(channel, accountViewModel) + delay(30_000) // Poll every 30 seconds } } - LiveStatusIndicator(isOnline = isOnline, modifier = modifier) + LiveStatusIndicator( + isOnline = isOnline, + modifier = modifier, + ) } private suspend fun checkChannelIsOnline(