diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt index 586719a14..ee853f4ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt @@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.encoders.HttpUrlFormatter @Composable fun MediaServerEditField( @@ -47,6 +48,12 @@ fun MediaServerEditField( onAddServer: (String) -> Unit, ) { var url by remember { mutableStateOf("") } + val validUrl by + remember { + derivedStateOf { + url.isNotBlank() && HttpUrlFormatter.isValidUrl(url) + } + } Row( verticalAlignment = Alignment.CenterVertically, @@ -73,20 +80,12 @@ fun MediaServerEditField( Button( onClick = { if (url.isNotBlank() && url != "/") { - onAddServer(url) + onAddServer(HttpUrlFormatter.normalize(url)) url = "" } }, shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = - if (url.isNotBlank()) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.placeholderText - }, - ), + enabled = validUrl, ) { Text(text = stringRes(id = R.string.add), color = Color.White) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index 9805e0d79..90bcbbb4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.actions.mediaServers +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account @@ -47,12 +48,17 @@ class MediaServersViewModel : ViewModel() { isModified = false _fileServers.update { val obtainedFileServers = obtainFileServers() ?: emptyList() - obtainedFileServers.map { serverUrl -> - Nip96MediaServers - .ServerName( - URIReference.parse(serverUrl).host.value, - serverUrl, - ) + obtainedFileServers.mapNotNull { serverUrl -> + try { + Nip96MediaServers + .ServerName( + URIReference.parse(serverUrl).host.value, + serverUrl, + ) + } catch (e: Exception) { + Log.d("MediaServersViewModel", "Invalid URL in NIP-96 server list") + null + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HttpUrlFormatter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HttpUrlFormatter.kt new file mode 100644 index 000000000..2e378683b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HttpUrlFormatter.kt @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2024 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.quartz.encoders + +import org.czeal.rfc3986.URIReference + +class HttpUrlFormatter { + companion object { + fun displayHost(url: String): String = + url + .trim() + .removePrefix("https://") + .removePrefix("http://") + .removeSuffix("/") + + fun displayUrl(url: String): String = + url + .trim() + .removePrefix("https://") + .removePrefix("http://") + .removeSuffix("/") + + fun addSchemeIfNeeded(url: String): String = + if (!url.startsWith("https://") && !url.startsWith("http://")) { + // TODO: How to identify relays on the local network? + val isLocalHost = url.contains("127.0.0.1") || url.contains("localhost") + if (url.endsWith(".onion") || url.endsWith(".onion/") || isLocalHost) { + "http://${url.trim()}" + } else { + "https://${url.trim()}" + } + } else { + url.trim() + } + + fun normalize(url: String): String { + val newUrl = addSchemeIfNeeded(url) + + return try { + URIReference.parse(newUrl).normalize().toString() + } catch (e: Exception) { + newUrl + } + } + + fun isValidUrl(url: String): Boolean = + runCatching { + URIReference.parse(addSchemeIfNeeded(url)) + }.isSuccess + } +}