From 5796052f36cd0c380f9c2f950376df7be722362c Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 15 Sep 2025 18:21:34 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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( From 6cffda09bb587e1f65f6ac3b38eb5e168f7b49ec Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 23 Sep 2025 11:59:59 +0000 Subject: [PATCH 4/4] New Crowdin translations by GitHub Action --- .../src/main/res/values-nl-rNL/strings.xml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml index 50d666ced..3db9ae5d1 100644 --- a/amethyst/src/main/res/values-nl-rNL/strings.xml +++ b/amethyst/src/main/res/values-nl-rNL/strings.xml @@ -148,6 +148,7 @@ De video is niet opgeslagen Afbeelding uploaden Neem een foto + Video opnemen Neem een bericht op Neem een bericht op Klik en houd vast om een bericht te opnemen @@ -163,6 +164,8 @@ Geblokkeerde gebruikers Nieuwe onderwerpen Discussies + Feed + Modwachtrij Notes Reacties Jouw @@ -437,6 +440,7 @@ Nee Volgerslijst Iedereen + Volg alle gebruikers Volgt via proxy In de buurt Globaal @@ -655,6 +659,8 @@ Uitleg aan leden De naam veranderen voor de nieuwe doelen. Plakken vanaf klembord + Invalide NIP-47 URI + De URI %1$s is geen geldige NIP-47 login-URI. Voor de interface van de app Donker-, Licht- of Systeemthema Automatisch afbeeldingen en GIF\'s laden @@ -672,6 +678,9 @@ Media toegevoegd aan je profielgalerij Gemaakt op Regels + Over ons + Richtlijnen + Moderators Login met Amber Status bijwerken Fout bij het parsen van foutberichten @@ -884,10 +893,13 @@ DM uploaden Relay-instellingen Openbare thuis relays + Gebruiker plaatst zijn content op deze relays Dit relay type slaat al je inhoud op. Amethyst stuurt je berichten hierheen en anderen gebruiken deze relays om je inhoud te vinden. Voeg tussen de 1 en 3 relays toe. Het gaat om persoonlijke relays, betaalde relays of openbare relays. Openbare inbox relays + Gebruiker ontvangt meldingen voor deze relays Dit type relay ontvangt alle relays, reacties, likes en zaps op je berichten. Ze kunnen betaalde of gratis relays zijn. Limieten ingesteld door de relay beheerder kunnen de meldingen die u ontvangt beperken voor het goede en het slechte. Als u bijvoorbeeld wordt aangevallen door spam, kunnen betaalde relays de spam filteren. Invoegen tussen de 1 en 3 relays. DM inbox relays + Gebruiker ontvangt DM\'s bij deze relays Voeg tussen de 1 en 3 relays toe om als uw privéinbox te dienen. Anderen gebruiken deze relays om DM\'s naar u te sturen. DM Inbox relays moeten een bericht van iedereen accepteren, maar alleen om berichten te downloaden. Goede opties zijn:\n - inbox.nostr.wine (betaald)\n - you.nostr1.com (persoonlijke relays - betaald) Privé relays Plaats tussen de 1 tot 3 relays om gebeurtenissen op te slaan die niemand anders kan zien, zoals uw Concepten en/of app instellingen. Idealiter zijn deze relays lokaal of vereist authenticatie voordat de inhoud van elke gebruiker wordt gedownload. @@ -1002,6 +1014,7 @@ Downloaden Kon bestand niet openen Geen torrent apps geïnstalleerd om het bestand te openen en te downloaden. + Evenement heeft niet genoeg informatie om een magnet-link te maken Selecteer een lijst om de feed te filteren Afmelden bij apparaatvergrendeling Privébericht @@ -1009,9 +1022,18 @@ Chat relay De relay die alle gebruikers van deze chat gebruiken om verbinding te maken Afbeelding delen… + Kan afbeelding niet delen, probeer het later opnieuw… Zoek hashtag: #%1$s Niet vertalen van Talen die hier getoond worden worden worden niet vertaald. Selecteer een taal om te verwijderen en het opnieuw te laten vertalen. Pauze Afspelen + Open dropdown-menu + Optie %1$s van %2$s + Feed-filter, %1$s geselecteerd + Feed-filter, %1$s + Crashrapport gevonden + Wilt u het recente crashrapport verzenden naar Amethyst in een DM? Er zal geen persoonlijke informatie gedeeld worden + Verzenden + Dit bericht zal verdwijnen in %1$d dagen