diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt index aab7ff6e9..5964bf660 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData @@ -71,11 +72,11 @@ fun RenderControlButtons( } AnimatedShareButton(controllerVisible, buttonPositionModifier.padding(end = Size165dp)) { popupExpanded, toggle -> - ShareMediaAction(accountViewModel = accountViewModel, popupExpanded, mediaData.videoUri, mediaData.callbackUri, null, null, null, mediaData.mimeType, toggle) + ShareMediaAction(accountViewModel = accountViewModel, popupExpanded, mediaData.videoUri, mediaData.callbackUri, null, null, null, mediaData.mimeType, toggle, content = MediaUrlVideo(url = mediaData.videoUri, mimeType = mediaData.mimeType, artworkUri = mediaData.artworkUri, authorName = mediaData.authorName, description = mediaData.title, uri = mediaData.callbackUri)) } } else { AnimatedShareButton(controllerVisible, buttonPositionModifier.padding(end = Size110dp)) { popupExpanded, toggle -> - ShareMediaAction(accountViewModel = accountViewModel, popupExpanded, mediaData.videoUri, mediaData.callbackUri, null, null, null, mediaData.mimeType, toggle) + ShareMediaAction(accountViewModel = accountViewModel, popupExpanded, mediaData.videoUri, mediaData.callbackUri, null, null, null, mediaData.mimeType, toggle, content = MediaUrlVideo(url = mediaData.videoUri, mimeType = mediaData.mimeType, artworkUri = mediaData.artworkUri, authorName = mediaData.authorName, description = mediaData.title, uri = mediaData.callbackUri)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 97d8e193d..720539ac6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -53,7 +53,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -69,6 +68,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.core.net.toUri +import androidx.lifecycle.viewModelScope import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage @@ -246,8 +246,8 @@ fun LocalImageView( } val ratio = remember(content) { content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.localFile.toString()) } - CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) { - if (it) { + CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) { imageVisible -> + if (imageVisible) { SubcomposeAsyncImage( model = content.localFile, contentDescription = content.description, @@ -552,11 +552,7 @@ fun ShowHash(content: MediaUrlContent) { } @Composable -fun WaitAndDisplay( - content: - @Composable() - (AnimatedVisibilityScope.() -> Unit), -) { +fun WaitAndDisplay(content: @Composable (AnimatedVisibilityScope.() -> Unit)) { val visible = remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -733,7 +729,7 @@ fun ShareMediaAction( onDismiss: () -> Unit, content: BaseMediaContent? = null, ) { - val scope = rememberCoroutineScope() + val scope = accountViewModel.viewModelScope // Track if video is downloading - hoisted here to block menu dismiss during download val isDownloadingVideo = remember { mutableStateOf(false) } @@ -939,7 +935,11 @@ private suspend fun shareVideoFile( // GlobalScope is intentional: cleanup must survive after share UI is dismissed. GlobalScope.launch(Dispatchers.IO) { delay(SHARED_VIDEO_CLEANUP_DELAY_MS) - sharedFile?.delete() + sharedFile?.let { file -> + if (!file.delete()) { + Log.w("ZoomableContentView", "Failed to delete shared file: ${file.path}") + } + } } withContext(Dispatchers.Main) { @@ -950,8 +950,14 @@ private suspend fun shareVideoFile( Log.w("ZoomableContentView", "Failed to share video: $videoUrl", e) // Clean up temp file on error - tempFile.delete() - sharedFile?.delete() + if (!tempFile.delete()) { + Log.w("ZoomableContentView", "Failed to delete temp file: ${tempFile.path}") + } + sharedFile?.let { file -> + if (!file.delete()) { + Log.w("ZoomableContentView", "Failed to delete shared file: ${file.path}") + } + } withContext(Dispatchers.Main) { Toast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index 5e4fc7b20..0be396cda 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -128,6 +128,8 @@ import kotlinx.collections.immutable.toImmutableList import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract +private const val HTTPS_PREFIX = "https://" + @Composable fun RelayInformationScreen( relayUrl: String, @@ -434,7 +436,7 @@ fun FeesCard( } payUrl?.let { val uri = LocalUriHandler.current - ClickableInfoRow(Icons.Default.Payment, stringRes(R.string.payments_url), it.removePrefix("https://")) { + ClickableInfoRow(Icons.Default.Payment, stringRes(R.string.payments_url), it.removePrefix(HTTPS_PREFIX)) { runCatching { uri.openUri(it) } @@ -583,8 +585,8 @@ fun SoftwareCard(relayInfo: Nip11RelayInformation) { Column(modifier = Modifier.padding(16.dp)) { val uri = LocalUriHandler.current relayInfo.software?.let { - if (it.contains("https://")) { - ClickableInfoRow(Icons.Default.Code, stringRes(R.string.software), it.removePrefix("git+https://").removePrefix("https://")) { + if (it.contains(HTTPS_PREFIX)) { + ClickableInfoRow(Icons.Default.Code, stringRes(R.string.software), it.removePrefix("git+https://").removePrefix(HTTPS_PREFIX)) { runCatching { uri.openUri(it.removePrefix("git+")) } @@ -809,7 +811,7 @@ fun PoliciesCard(relay: Nip11RelayInformation) { val pp = relay.privacy_policy if (pp != null) { - ClickableInfoRow(Icons.Default.PrivacyTip, stringRes(R.string.privacy_policy), pp.removePrefix("https://")) { + ClickableInfoRow(Icons.Default.PrivacyTip, stringRes(R.string.privacy_policy), pp.removePrefix(HTTPS_PREFIX)) { runCatching { uri.openUri(pp) } @@ -820,7 +822,7 @@ fun PoliciesCard(relay: Nip11RelayInformation) { val ts = relay.terms_of_service if (ts != null) { - ClickableInfoRow(Icons.Default.Gavel, stringRes(R.string.terms_and_conditions), ts.removePrefix("https://")) { + ClickableInfoRow(Icons.Default.Gavel, stringRes(R.string.terms_and_conditions), ts.removePrefix(HTTPS_PREFIX)) { runCatching { uri.openUri(ts) } diff --git a/amethyst/src/main/res/values-ca-rES/strings.xml b/amethyst/src/main/res/values-ca-rES/strings.xml new file mode 100644 index 000000000..66a570486 --- /dev/null +++ b/amethyst/src/main/res/values-ca-rES/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 9015e0965..77c479819 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1202,6 +1202,9 @@ anz der Bedingungen ist erforderlich Das Relais, mit dem sich alle Benutzer dieses Chats verbinden Bild teilen… Bild kann nicht geteilt werden, bitte versuchen Sie es später erneut… + Video teilen… + Video kann nicht geteilt werden, bitte versuchen Sie es später erneut… + Video wird heruntergeladen… Suche Hashtag: #%1$s Nicht übersetzen von Die hier angezeigten Sprachen werden nicht übersetzt. Wählen Sie eine Sprache, um sie zu entfernen und lassen Sie sie erneut übersetzen. diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index dece24a0f..5c4b548ec 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1202,6 +1202,9 @@ Az átjátszó, amelyhez a csevegés összes felhasználója csatlakozik Kép megosztása… Nem lehetett megosztani a képet, próbálja meg újra később… + Videó megosztása… + Nem sikerült megosztani a videót, próbálja meg újra később… + Videó letöltése… Hashtag keresése: #%1$s Innentől NE fordítsa le Az itt látható nyelvek nem lesznek lefordítva. Az eltávolításához és az újbóli fordításhoz válasszon ki egy nyelvet. diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index eaf7cce96..557f88dc0 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -499,7 +499,7 @@ Nie Lista obserwowanych Obserwowane - Obserwujące + Obserwowane kontakty Domyślna lista obserwowanych Obserwuje przez proxy W pobliżu @@ -1199,6 +1199,9 @@ Transmiter, z którym łączą się wszyscy użytkownicy tego czatu Udostępnij zdjęcie… Nie można udostępnić obrazu, spróbuj ponownie później… + Udostępnij film… + Nie można udostępnić filmu, spróbuj ponownie później… + Pobieranie filmu… Szukaj tagu: #%1$s Nie tłumacz z Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index a90421341..09aa6def8 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1197,6 +1197,9 @@ O relé a qual todos os usuários deste chat se conectam Compartilhar imagem… Não é possível compartilhar a imagem, por favor tente novamente mais tarde… + Compartilhar vídeo… + Não foi possível compartilhar o vídeo, tente novamente mais tarde… + Baixando vídeo… Pesquisar hashtag: #%1$s Não Traduzir de Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 44571cb2a..e8be7b477 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1196,6 +1196,9 @@ Reläet som alla användare av den här chatten ansluter till Dela bild… Kunde inte dela bilden, försök igen senare… + Dela video… + Det gick inte att dela videon, försök igen senare… + Laddar ner videon… Sök hashtag: #%1$s Översätt inte från Språk som visas här kommer inte att översättas. Välj ett språk för att ta bort det och få det översatt igen. diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 7f427f586..23e488f30 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -1202,6 +1202,9 @@ 此聊天所有用户都连接到的中继 分享图片… 无法分享图片,请稍后重试… + 分享视频… + 无法分享视频,请稍后重试… + 正下载视频… 搜索话题标签:#%1$s 不要翻译 此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。 diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt index 556dda5de..618491580 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt @@ -52,15 +52,15 @@ class ManualFilterDeserializer { } return Filter( - ids = jsonObject.get("ids").mapNotNull { it.asTextOrNull() }, - authors = jsonObject.get("authors").mapNotNull { it.asTextOrNull() }, - kinds = jsonObject.get("kinds").mapNotNull { it.asIntOrNull() }, - tags = tagsIn.associateWith { jsonObject.get(it).mapNotNull { it.asTextOrNull() } }, - tagsAll = tagsAll.associateWith { jsonObject.get(it).mapNotNull { it.asTextOrNull() } }, - since = jsonObject.get("since").asLongOrNull(), - until = jsonObject.get("until").asLongOrNull(), - limit = jsonObject.get("limit").asIntOrNull(), - search = jsonObject.get("search").asTextOrNull(), + ids = jsonObject.get("ids")?.mapNotNull { it.asTextOrNull() }, + authors = jsonObject.get("authors")?.mapNotNull { it.asTextOrNull() }, + kinds = jsonObject.get("kinds")?.mapNotNull { it.asIntOrNull() }, + tags = tagsIn.associateWith { jsonObject.get("#$it").mapNotNull { it.asTextOrNull() } }, + tagsAll = tagsAll.associateWith { jsonObject.get("&$it").mapNotNull { it.asTextOrNull() } }, + since = jsonObject.get("since")?.asLongOrNull(), + until = jsonObject.get("until")?.asLongOrNull(), + limit = jsonObject.get("limit")?.asIntOrNull(), + search = jsonObject.get("search")?.asTextOrNull(), ) } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt index b9cc65956..b9b732596 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor @@ -152,4 +153,28 @@ class JacksonMapperTest { assertContentEquals(tag, deserialized.tags[index]) } } + + @Test + fun shouldNotThrowExceptionWhenDeserializingEmptyFilter() { + val json = Filter().toJson() + val deserialized = JacksonMapper.fromJsonTo(json) + + assertEquals(null, deserialized.ids) + } + + @Test + fun shouldNotThrowExceptionWhenDeserializingFilterTags() { + val expectedTagValue = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da" + val json = + Filter( + tags = mapOf("p" to listOf(expectedTagValue)), + tagsAll = mapOf("p" to listOf(expectedTagValue)), + ).toJson() + val deserialized = JacksonMapper.fromJsonTo(json) + + assertEquals(true, deserialized.tags?.keys?.contains("p")) + assertEquals(listOf(expectedTagValue), deserialized.tags?.get("p")) + assertEquals(true, deserialized.tagsAll?.keys?.contains("p")) + assertEquals(listOf(expectedTagValue), deserialized.tagsAll?.get("p")) + } }