From 997a270a84ee7269c4d0dfb58968aaa3d83ba749 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 02:30:33 +0000 Subject: [PATCH] feat: add export button to relay settings screen Adds a share icon button in the top bar of the relay settings screen that exports all relay configurations (except Connected Relays) as human-readable text via Android's share intent. Each section includes a header and description, with one relay URL per line. https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM --- .../ui/navigation/topbars/ActionTopBar.kt | 6 + .../loggedIn/relays/AllRelayListScreen.kt | 193 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 2 + 3 files changed, 201 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt index a16b5925e..a49514dc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt @@ -43,6 +43,7 @@ fun ActionTopBar( isActive: () -> Boolean = { true }, onCancel: () -> Unit, onPost: () -> Unit, + additionalActions: @Composable (() -> Unit)? = null, ) { ShorterTopAppBar( title = { @@ -63,6 +64,9 @@ fun ActionTopBar( ) }, actions = { + if (additionalActions != null) { + additionalActions() + } Button( modifier = HalfHorzPadding, enabled = isActive(), @@ -100,12 +104,14 @@ fun SavingTopBar( isActive: () -> Boolean = { true }, onCancel: () -> Unit, onPost: () -> Unit, + additionalActions: @Composable (() -> Unit)? = null, ) = ActionTopBar( titleRes = titleRes, postRes = R.string.save, isActive = isActive, onCancel = onCancel, onPost = onPost, + additionalActions = additionalActions, ) @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 508c8ae09..ef41607de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays +import android.content.Context +import android.content.Intent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets @@ -27,7 +29,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -36,6 +42,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -77,6 +84,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.theme.grayText @Composable @@ -175,10 +183,36 @@ fun MappedAllRelayListView( val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() + val context = LocalContext.current + Scaffold( topBar = { SavingTopBar( titleRes = R.string.relay_settings, + additionalActions = { + IconButton(onClick = { + exportRelaySettings( + context = context, + homeRelays = homeFeedState, + notifRelays = notifFeedState, + dmRelays = dmFeedState, + privateOutboxRelays = privateOutboxFeedState, + proxyRelays = proxyRelays, + broadcastRelays = broadcastRelays, + indexerRelays = indexerRelays, + searchRelays = searchFeedState, + localRelays = localFeedState, + trustedRelays = trustedFeedState, + favoriteRelays = relayFeedsFeedState, + blockedRelays = blockedFeedState, + ) + }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringRes(R.string.export_relay_settings), + ) + } + }, onCancel = { dmViewModel.clear() nip65ViewModel.clear() @@ -447,3 +481,162 @@ fun SettingsCategoryWithButton( action() } } + +private fun formatRelaySection( + title: String, + description: String, + relays: List, + builder: StringBuilder, +) { + if (relays.isEmpty()) return + builder.appendLine("## $title") + builder.appendLine("# $description") + builder.appendLine() + relays.forEach { relay -> + builder.appendLine(relay.relay.url) + } + builder.appendLine() +} + +fun buildRelaySettingsText( + context: Context, + homeRelays: List, + notifRelays: List, + dmRelays: List, + privateOutboxRelays: List, + proxyRelays: List, + broadcastRelays: List, + indexerRelays: List, + searchRelays: List, + localRelays: List, + trustedRelays: List, + favoriteRelays: List, + blockedRelays: List, +): String { + val builder = StringBuilder() + builder.appendLine("# ${context.getString(R.string.relay_settings)}") + builder.appendLine() + + formatRelaySection( + context.getString(R.string.public_home_section), + context.getString(R.string.public_home_section_explainer), + homeRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.public_notif_section), + context.getString(R.string.public_notif_section_explainer), + notifRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.private_inbox_section), + context.getString(R.string.private_inbox_section_explainer), + dmRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.private_outbox_section), + context.getString(R.string.private_outbox_section_explainer), + privateOutboxRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.proxy_section), + context.getString(R.string.proxy_section_explainer), + proxyRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.broadcast_section), + context.getString(R.string.broadcast_section_explainer), + broadcastRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.indexer_section), + context.getString(R.string.indexer_section_explainer), + indexerRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.search_section), + context.getString(R.string.search_section_explainer), + searchRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.local_section), + context.getString(R.string.local_section_explainer), + localRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.trusted_section), + context.getString(R.string.trusted_section_explainer), + trustedRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.favorite_section), + context.getString(R.string.favorite_section_explainer), + favoriteRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.blocked_section), + context.getString(R.string.blocked_section_explainer), + blockedRelays, + builder, + ) + + return builder.toString().trimEnd() +} + +fun exportRelaySettings( + context: Context, + homeRelays: List, + notifRelays: List, + dmRelays: List, + privateOutboxRelays: List, + proxyRelays: List, + broadcastRelays: List, + indexerRelays: List, + searchRelays: List, + localRelays: List, + trustedRelays: List, + favoriteRelays: List, + blockedRelays: List, +) { + val text = + buildRelaySettingsText( + context, + homeRelays, + notifRelays, + dmRelays, + privateOutboxRelays, + proxyRelays, + broadcastRelays, + indexerRelays, + searchRelays, + localRelays, + trustedRelays, + favoriteRelays, + blockedRelays, + ) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + putExtra(Intent.EXTRA_TITLE, context.getString(R.string.export_relay_settings)) + } + + val shareIntent = + Intent.createChooser( + sendIntent, + context.getString(R.string.export_relay_settings), + ) + context.startActivity(shareIntent) +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 72f3724dc..02349fa0d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1379,6 +1379,8 @@ Blocked Relays Amethyst will never connect to these relays + Export relay settings + Zap the Devs! Your donation helps us make a difference. Every sat counts! Donate Now