diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt index 116e36b8b1..d31be56224 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountMarmotActions.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.quartz.marmot.appComponents.BlobStoreEndpointV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaPolicyV2 import com.vitorpamplona.quartz.marmot.appComponents.GroupAvatarUrlV1 @@ -962,13 +963,40 @@ class AccountMarmotActions( * * The endpoints come from the account's own Blossom server list, because a * policy naming servers the uploader does not use would describe a group - * nobody can actually post media to. + * nobody can actually post media to. An account without one uploads to the + * default list (the same one the upload picker offers and falls back + * through), so that is what its policy names. */ + fun marmotMediaPolicyServers(): List = + account.blossomServers.flow.value + .ifEmpty { + account.blossomServers.hostNameFlow.value + .filter { it.type == ServerType.Blossom } + .map { it.baseUrl } + }.mapNotNull { normalizedPolicyBaseUrl(it) } + .distinct() + .take(EncryptedMediaPolicyV2.MAX_ENTRIES) + + /** + * [url] in the byte-exact form the media policy requires, or null when it has none. + * The component rejects a base URL that isn't its own WHATWG serialization, so the + * everyday spelling without a trailing slash (`https://cdn.nostrcheck.me`) failed the + * whole commit. + */ + private fun normalizedPolicyBaseUrl(url: String): String? = + try { + MarmotWebUrl.normalize(url, allowHttp = true, label = "base_url").also { + EncryptedMediaPolicyV2.requireNormalizedBaseUrl(it) + } + } catch (e: IllegalArgumentException) { + null + } + suspend fun enableMarmotEncryptedMediaV2(nostrGroupId: HexKey) { val manager = account.marmotManager ?: return if (!account.isWriteable()) return - val servers = account.blossomServers.flow.value + val servers = marmotMediaPolicyServers() require(servers.isNotEmpty()) { "Cannot enable encrypted media without at least one Blossom server configured" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 0439256b55..6439cc7e86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -2537,9 +2537,7 @@ class AccountViewModel( fun marmotUsesEncryptedMediaV2(nostrGroupId: String): Boolean = account.marmotManager?.encryptedMediaPolicy(nostrGroupId) != null /** True when this account has somewhere to upload a group's encrypted media. */ - fun hasBlossomServers(): Boolean = - account.blossomServers.flow.value - .isNotEmpty() + fun hasBlossomServers(): Boolean = account.marmot.marmotMediaPolicyServers().isNotEmpty() suspend fun enableMarmotEncryptedMediaV2(nostrGroupId: String) { account.marmot.enableMarmotEncryptedMediaV2(nostrGroupId) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index 67eb18089d..cae9a3f162 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt @@ -67,9 +67,11 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.RandomInstance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException @Composable fun CreateGroupScreen( @@ -136,6 +138,17 @@ fun CreateGroupScreen( description = groupDescription.trim(), icon = iconChange, ) + // Commit the encrypted-media policy while we are the only member. White Noise + // (MDK) refuses to send any attachment in a group without one + // ("group does not require encrypted media"), and joiners learn it from the + // Welcome, so nobody has to apply a later commit to be able to share media. + // Best-effort: a group without it still works for text and legacy MIP-04. + try { + accountViewModel.enableMarmotEncryptedMediaV2(nostrGroupId) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("CreateGroupScreen") { "Could not enable encrypted media for $nostrGroupId: ${e.message}" } + } nav.popUpTo(Route.MarmotGroupChat(nostrGroupId), Route.CreateMarmotGroup::class) } catch (e: Exception) { isCreating = false