Merge branch 'vitorpamplona:main' into kmp-completeness

This commit is contained in:
KotlinGeekDev
2026-02-02 16:43:30 +01:00
committed by GitHub
12 changed files with 83 additions and 29 deletions
@@ -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))
}
}
}
@@ -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
@@ -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)
}
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
@@ -1202,6 +1202,9 @@ anz der Bedingungen ist erforderlich</string>
<string name="group_relay_explanation">Das Relais, mit dem sich alle Benutzer dieses Chats verbinden</string>
<string name="share_image">Bild teilen…</string>
<string name="unable_to_share_image">Bild kann nicht geteilt werden, bitte versuchen Sie es später erneut…</string>
<string name="share_video">Video teilen…</string>
<string name="unable_to_share_video">Video kann nicht geteilt werden, bitte versuchen Sie es später erneut…</string>
<string name="downloading_video_for_sharing">Video wird heruntergeladen…</string>
<string name="search_by_hashtag">Suche Hashtag: #%1$s</string>
<string name="dont_translate_from">Nicht übersetzen von</string>
<string name="dont_translate_from_description">Die hier angezeigten Sprachen werden nicht übersetzt. Wählen Sie eine Sprache, um sie zu entfernen und lassen Sie sie erneut übersetzen.</string>
@@ -1202,6 +1202,9 @@
<string name="group_relay_explanation">Az átjátszó, amelyhez a csevegés összes felhasználója csatlakozik</string>
<string name="share_image">Kép megosztása…</string>
<string name="unable_to_share_image">Nem lehetett megosztani a képet, próbálja meg újra később…</string>
<string name="share_video">Videó megosztása…</string>
<string name="unable_to_share_video">Nem sikerült megosztani a videót, próbálja meg újra később…</string>
<string name="downloading_video_for_sharing">Videó letöltése…</string>
<string name="search_by_hashtag">Hashtag keresése: #%1$s</string>
<string name="dont_translate_from">Innentől NE fordítsa le</string>
<string name="dont_translate_from_description">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.</string>
@@ -499,7 +499,7 @@
<string name="no">Nie</string>
<string name="follow_list_selection">Lista obserwowanych</string>
<string name="follow_list_kind3follows">Obserwowane</string>
<string name="follow_list_kind3follows_users_only">Obserwujące</string>
<string name="follow_list_kind3follows_users_only">Obserwowane kontakty</string>
<string name="follow_list_kind3_follows_users_only">Domyślna lista obserwowanych</string>
<string name="follow_list_kind3follows_proxy">Obserwuje przez proxy</string>
<string name="follow_list_aroundme">W pobliżu</string>
@@ -1199,6 +1199,9 @@
<string name="group_relay_explanation">Transmiter, z którym łączą się wszyscy użytkownicy tego czatu</string>
<string name="share_image">Udostępnij zdjęcie…</string>
<string name="unable_to_share_image">Nie można udostępnić obrazu, spróbuj ponownie później…</string>
<string name="share_video">Udostępnij film…</string>
<string name="unable_to_share_video">Nie można udostępnić filmu, spróbuj ponownie później…</string>
<string name="downloading_video_for_sharing">Pobieranie filmu…</string>
<string name="search_by_hashtag">Szukaj tagu: #%1$s</string>
<string name="dont_translate_from">Nie tłumacz z</string>
<string name="dont_translate_from_description">Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych.</string>
@@ -1197,6 +1197,9 @@
<string name="group_relay_explanation">O relé a qual todos os usuários deste chat se conectam</string>
<string name="share_image">Compartilhar imagem…</string>
<string name="unable_to_share_image">Não é possível compartilhar a imagem, por favor tente novamente mais tarde…</string>
<string name="share_video">Compartilhar vídeo…</string>
<string name="unable_to_share_video">Não foi possível compartilhar o vídeo, tente novamente mais tarde…</string>
<string name="downloading_video_for_sharing">Baixando vídeo…</string>
<string name="search_by_hashtag">Pesquisar hashtag: #%1$s</string>
<string name="dont_translate_from">Não Traduzir de</string>
<string name="dont_translate_from_description">Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente.</string>
@@ -1196,6 +1196,9 @@
<string name="group_relay_explanation">Reläet som alla användare av den här chatten ansluter till</string>
<string name="share_image">Dela bild…</string>
<string name="unable_to_share_image">Kunde inte dela bilden, försök igen senare…</string>
<string name="share_video">Dela video…</string>
<string name="unable_to_share_video">Det gick inte att dela videon, försök igen senare…</string>
<string name="downloading_video_for_sharing">Laddar ner videon…</string>
<string name="search_by_hashtag">Sök hashtag: #%1$s</string>
<string name="dont_translate_from">Översätt inte från</string>
<string name="dont_translate_from_description">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.</string>
@@ -1202,6 +1202,9 @@
<string name="group_relay_explanation">此聊天所有用户都连接到的中继</string>
<string name="share_image">分享图片…</string>
<string name="unable_to_share_image">无法分享图片,请稍后重试…</string>
<string name="share_video">分享视频…</string>
<string name="unable_to_share_video">无法分享视频,请稍后重试…</string>
<string name="downloading_video_for_sharing">正下载视频…</string>
<string name="search_by_hashtag">搜索话题标签:#%1$s</string>
<string name="dont_translate_from">不要翻译</string>
<string name="dont_translate_from_description">此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。</string>
@@ -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(),
)
}
}
@@ -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<Filter>(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<Filter>(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"))
}
}