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
This commit is contained in:
Claude
2026-03-13 02:30:33 +00:00
parent 198aa0971c
commit 997a270a84
3 changed files with 201 additions and 0 deletions
@@ -43,6 +43,7 @@ fun ActionTopBar(
isActive: () -> Boolean = { true }, isActive: () -> Boolean = { true },
onCancel: () -> Unit, onCancel: () -> Unit,
onPost: () -> Unit, onPost: () -> Unit,
additionalActions: @Composable (() -> Unit)? = null,
) { ) {
ShorterTopAppBar( ShorterTopAppBar(
title = { title = {
@@ -63,6 +64,9 @@ fun ActionTopBar(
) )
}, },
actions = { actions = {
if (additionalActions != null) {
additionalActions()
}
Button( Button(
modifier = HalfHorzPadding, modifier = HalfHorzPadding,
enabled = isActive(), enabled = isActive(),
@@ -100,12 +104,14 @@ fun SavingTopBar(
isActive: () -> Boolean = { true }, isActive: () -> Boolean = { true },
onCancel: () -> Unit, onCancel: () -> Unit,
onPost: () -> Unit, onPost: () -> Unit,
additionalActions: @Composable (() -> Unit)? = null,
) = ActionTopBar( ) = ActionTopBar(
titleRes = titleRes, titleRes = titleRes,
postRes = R.string.save, postRes = R.string.save,
isActive = isActive, isActive = isActive,
onCancel = onCancel, onCancel = onCancel,
onPost = onPost, onPost = onPost,
additionalActions = additionalActions,
) )
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -20,6 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets 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.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -36,6 +42,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel 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.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
@Composable @Composable
@@ -175,10 +183,36 @@ fun MappedAllRelayListView(
val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle()
val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle()
val context = LocalContext.current
Scaffold( Scaffold(
topBar = { topBar = {
SavingTopBar( SavingTopBar(
titleRes = R.string.relay_settings, 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 = { onCancel = {
dmViewModel.clear() dmViewModel.clear()
nip65ViewModel.clear() nip65ViewModel.clear()
@@ -447,3 +481,162 @@ fun SettingsCategoryWithButton(
action() action()
} }
} }
private fun formatRelaySection(
title: String,
description: String,
relays: List<BasicRelaySetupInfo>,
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<BasicRelaySetupInfo>,
notifRelays: List<BasicRelaySetupInfo>,
dmRelays: List<BasicRelaySetupInfo>,
privateOutboxRelays: List<BasicRelaySetupInfo>,
proxyRelays: List<BasicRelaySetupInfo>,
broadcastRelays: List<BasicRelaySetupInfo>,
indexerRelays: List<BasicRelaySetupInfo>,
searchRelays: List<BasicRelaySetupInfo>,
localRelays: List<BasicRelaySetupInfo>,
trustedRelays: List<BasicRelaySetupInfo>,
favoriteRelays: List<BasicRelaySetupInfo>,
blockedRelays: List<BasicRelaySetupInfo>,
): 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<BasicRelaySetupInfo>,
notifRelays: List<BasicRelaySetupInfo>,
dmRelays: List<BasicRelaySetupInfo>,
privateOutboxRelays: List<BasicRelaySetupInfo>,
proxyRelays: List<BasicRelaySetupInfo>,
broadcastRelays: List<BasicRelaySetupInfo>,
indexerRelays: List<BasicRelaySetupInfo>,
searchRelays: List<BasicRelaySetupInfo>,
localRelays: List<BasicRelaySetupInfo>,
trustedRelays: List<BasicRelaySetupInfo>,
favoriteRelays: List<BasicRelaySetupInfo>,
blockedRelays: List<BasicRelaySetupInfo>,
) {
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)
}
+2
View File
@@ -1379,6 +1379,8 @@
<string name="blocked_section">Blocked Relays</string> <string name="blocked_section">Blocked Relays</string>
<string name="blocked_section_explainer">Amethyst will never connect to these relays</string> <string name="blocked_section_explainer">Amethyst will never connect to these relays</string>
<string name="export_relay_settings">Export relay settings</string>
<string name="zap_the_devs_title">Zap the Devs!</string> <string name="zap_the_devs_title">Zap the Devs!</string>
<string name="zap_the_devs_description">Your donation helps us make a difference. Every sat counts!</string> <string name="zap_the_devs_description">Your donation helps us make a difference. Every sat counts!</string>
<string name="donate_now">Donate Now</string> <string name="donate_now">Donate Now</string>