Moves the account import idea to a route after login

This commit is contained in:
Vitor Pamplona
2026-03-10 10:19:18 -04:00
parent ff4356ce31
commit b8f9b18506
7 changed files with 115 additions and 167 deletions
@@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.ListOfPeopleList
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
@@ -177,6 +178,8 @@ fun AppNavigation(
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) } composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) } composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) } composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.ImportFollows> { ImportFollowListScreen(accountViewModel, nav) }
composableFromEndArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
composableFromEndArgs<Route.UpdateZapAmount> { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs<Route.UpdateZapAmount> { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) }
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) } composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
@@ -53,6 +53,8 @@ sealed class Route {
@Serializable object BookmarkGroups : Route() @Serializable object BookmarkGroups : Route()
@Serializable object ImportFollows : Route()
@Serializable data class BookmarkGroupView( @Serializable data class BookmarkGroupView(
val dTag: String, val dTag: String,
val bookmarkType: BookmarkType, val bookmarkType: BookmarkType,
@@ -29,27 +29,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.followimport.Kind3EventData
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoggedInPage import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoggedInPage
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
import com.vitorpamplona.amethyst.ui.screen.signup.ImportFollowListSection
import com.vitorpamplona.amethyst.ui.screen.signup.ImportFollowListViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable @Composable
fun AccountScreen(accountSessionManager: AccountSessionManager) { fun AccountScreen(accountSessionManager: AccountSessionManager) {
@@ -75,11 +64,7 @@ fun AccountScreen(accountSessionManager: AccountSessionManager) {
} }
is AccountState.LoggedIn -> { is AccountState.LoggedIn -> {
if (state.isNewAccount) { LoggedInSetup(state, accountSessionManager)
NewAccountImportFollowsSetup(state.account, accountSessionManager)
} else {
LoggedInSetup(state, accountSessionManager)
}
} }
} }
} }
@@ -139,80 +124,3 @@ fun LoggedInSetup(
) )
} }
} }
@Composable
fun NewAccountImportFollowsSetup(
account: Account,
accountSessionManager: AccountSessionManager,
) {
val importViewModel: ImportFollowListViewModel = viewModel()
LaunchedEffect(account) {
importViewModel.configure(
fetchEvent = { kind, author, limit, onEvent ->
val filter =
Filter(
kinds = listOf(kind),
authors = listOf(author),
limit = limit,
)
val relayUrls =
listOf(
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band",
"wss://purplepag.es",
)
val filterMap =
relayUrls.associate { url ->
RelayUrlNormalizer.normalize(url) to listOf(filter)
}
val listener =
object : com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener {
override fun onEvent(
event: com.vitorpamplona.quartz.nip01Core.core.Event,
isLive: Boolean,
relay: com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl,
forFilters: List<Filter>?,
) {
if (event is ContactListEvent) {
onEvent(
Kind3EventData(
pTags =
event.tags
.filter { it.size >= 2 && it[0] == "p" }
.map { it.drop(1) },
createdAt = event.createdAt,
),
)
}
}
}
val subId =
com.vitorpamplona.quartz.nip01Core.relay.client.single
.newSubId()
Amethyst.instance.client.openReqSubscription(subId, filterMap, listener)
AutoCloseable { Amethyst.instance.client.close(subId) }
},
)
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
ImportFollowListSection(
onFollowsApplied = { entries ->
withContext(Dispatchers.IO) {
for (entry in entries) {
val user = account.cache.getOrCreateUser(entry.pubkeyHex)
account.follow(user)
}
}
},
onSkip = { accountSessionManager.finishNewAccountSetup() },
onDone = { accountSessionManager.finishNewAccountSetup() },
viewModel = importViewModel,
)
}
}
@@ -87,7 +87,6 @@ sealed class AccountState {
class LoggedIn( class LoggedIn(
val account: Account, val account: Account,
var route: Route? = null, var route: Route? = null,
val isNewAccount: Boolean = false,
) : AccountState() ) : AccountState()
} }
@@ -184,20 +183,10 @@ class AccountSessionManager(
fun startUI( fun startUI(
accountSettings: AccountSettings, accountSettings: AccountSettings,
route: Route? = null, route: Route? = null,
isNewAccount: Boolean = false,
) { ) {
val account = accountsCache.loadAccount(accountSettings) val account = accountsCache.loadAccount(accountSettings)
_accountContent.update { _accountContent.update {
AccountState.LoggedIn(account, route, isNewAccount = isNewAccount) AccountState.LoggedIn(account, route)
}
}
fun finishNewAccountSetup() {
val current = _accountContent.value
if (current is AccountState.LoggedIn && current.isNewAccount) {
_accountContent.update {
AccountState.LoggedIn(current.account, current.route, isNewAccount = false)
}
} }
} }
@@ -289,7 +278,7 @@ class AccountSessionManager(
localPreferences.setDefaultAccount(accountSettings) localPreferences.setDefaultAccount(accountSettings)
startUI(accountSettings, isNewAccount = true) startUI(accountSettings, route = Route.ImportFollows)
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
delay(2000) // waits for the new user to connect to the new relays. delay(2000) // waits for the new user to connect to the new relays.
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.signup package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
@@ -50,8 +50,6 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.outlined.Circle import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -59,6 +57,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -72,20 +71,59 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.followimport.FollowEntry import com.vitorpamplona.amethyst.service.followimport.FollowEntry
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// ── Public entry points ──────────────────────────────────────────────── @Composable
fun ImportFollowListScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val importViewModel: ImportFollowListViewModel = viewModel()
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopBarExtensibleWithBackButton(
title = {},
popBack = nav::popBack,
)
},
) { padding ->
Column(
Modifier
.fillMaxSize()
.padding(padding),
) {
ImportFollowListSection(
onFollowsApplied = { entries ->
withContext(Dispatchers.IO) {
for (entry in entries) {
val user = accountViewModel.getOrCreateUser(entry.pubkeyHex)
accountViewModel.follow(user)
}
}
},
onSkip = nav::popBack,
onDone = nav::popBack,
viewModel = importViewModel,
)
}
}
}
/**
* Embeddable section for the signup wizard.
*/
@Composable @Composable
fun ImportFollowListSection( fun ImportFollowListSection(
onFollowsApplied: suspend (List<FollowEntry>) -> Unit, onFollowsApplied: suspend (List<FollowEntry>) -> Unit,
@@ -117,11 +155,11 @@ fun ImportFollowListSection(
} }
is ImportFollowState.Resolving -> { is ImportFollowState.Resolving -> {
LoadingIndicator("Resolving ${s.identifier}") LoadingIndicator(stringResource(R.string.resolving, s.identifier))
} }
is ImportFollowState.Fetching -> { is ImportFollowState.Fetching -> {
LoadingIndicator("Fetching follow list…") LoadingIndicator(stringResource(R.string.fetching_follow_list))
} }
is ImportFollowState.Preview -> { is ImportFollowState.Preview -> {
@@ -133,7 +171,7 @@ fun ImportFollowListSection(
} }
is ImportFollowState.Applying -> { is ImportFollowState.Applying -> {
LoadingIndicator("Following ${s.count} accounts…") LoadingIndicator(stringResource(R.string.following_accounts, s.count))
} }
is ImportFollowState.Done -> { is ImportFollowState.Done -> {
@@ -151,30 +189,6 @@ fun ImportFollowListSection(
} }
} }
/**
* Standalone dialog for post-signup use (settings / profile screen).
*/
@Composable
fun ImportFollowListDialog(
onDismiss: () -> Unit,
onFollowsApplied: suspend (List<FollowEntry>) -> Unit,
) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier.fillMaxWidth().padding(8.dp),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
) {
ImportFollowListSection(
onFollowsApplied = onFollowsApplied,
onSkip = onDismiss,
onDone = onDismiss,
modifier = Modifier.height(600.dp),
)
}
}
}
// ── Internal composables ─────────────────────────────────────────────── // ── Internal composables ───────────────────────────────────────────────
@Composable @Composable
@@ -189,14 +203,14 @@ private fun ImportHeader() {
) )
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Text( Text(
"Import Follow List", stringResource(R.string.import_follow_list),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
"Start with a great feed by following the same people as someone you trust.", stringResource(R.string.start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -215,15 +229,15 @@ private fun InputSection(
OutlinedTextField( OutlinedTextField(
value = identifier, value = identifier,
onValueChange = { identifier = it }, onValueChange = { identifier = it },
label = { Text("Profile to import from") }, label = { Text(stringResource(R.string.profile_to_import_from)) },
placeholder = { Text("npub1…, alice@example.com, or example.bit") }, placeholder = { Text(stringResource(R.string.name_search_npub1_alice_example_com)) },
singleLine = true, singleLine = true,
enabled = enabled, enabled = enabled,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
supportingText = { supportingText = {
Text( Text(
"Supports npub, NIP-05, hex, and Namecoin (.bit / d/ / id/)", stringResource(R.string.supports_npub_nip_05_hex_and_namecoin_bit_d_id),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
) )
@@ -244,7 +258,7 @@ private fun InputSection(
enabled = enabled && identifier.isNotBlank(), enabled = enabled && identifier.isNotBlank(),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
) { Text("Look Up Follow List") } ) { Text(stringResource(R.string.look_up_follow_list)) }
} }
} }
@@ -253,16 +267,14 @@ private fun IdleHint() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(24.dp)) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(24.dp)) {
Text( Text(
"Tip", stringResource(R.string.tip),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
"Enter the profile of a friend or community leader. " + stringResource(R.string.import_follows_tips),
"You can use their npub, NIP-05 address, or a Namecoin name " +
"like alice@example.bit or id/alice for blockchain-verified identities.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -303,17 +315,19 @@ private fun PreviewList(
// Summary // Summary
Row( Row(
Modifier.fillMaxWidth().padding(vertical = 4.dp), Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
"${state.totalCount} accounts found", stringResource(R.string.accounts_found, state.totalCount),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
Text( Text(
"${state.selectedCount} selected", stringResource(R.string.selected, state.selectedCount),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
@@ -321,7 +335,10 @@ private fun PreviewList(
// Select all // Select all
Row( Row(
Modifier.fillMaxWidth().clickable { onSelectAll(state.selectedCount < state.totalCount) }.padding(vertical = 8.dp), Modifier
.fillMaxWidth()
.clickable { onSelectAll(state.selectedCount < state.totalCount) }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Checkbox( Checkbox(
@@ -362,7 +379,7 @@ private fun NamecoinResolvedBadge(namecoinSource: String) {
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Column { Column {
Text( Text(
"Resolved via Namecoin", stringResource(R.string.resolved_via_namecoin),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color(0xFF4A90D9), color = Color(0xFF4A90D9),
@@ -383,7 +400,10 @@ private fun FollowEntryRow(
onToggle: () -> Unit, onToggle: () -> Unit,
) { ) {
Row( Row(
Modifier.fillMaxWidth().clickable(onClick = onToggle).padding(vertical = 6.dp, horizontal = 4.dp), Modifier
.fillMaxWidth()
.clickable(onClick = onToggle)
.padding(vertical = 6.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
@@ -399,7 +419,10 @@ private fun FollowEntryRow(
) )
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Box( Box(
Modifier.size(32.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer), Modifier
.size(32.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text( Text(
@@ -455,13 +478,13 @@ private fun DoneMessage(
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
"Now following $count accounts", stringResource(R.string.now_following_accounts, count),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text( Text(
"Your feed is ready.", stringResource(R.string.your_feed_is_ready),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -499,14 +522,14 @@ private fun BottomActions(
when (state) { when (state) {
is ImportFollowState.Preview -> { is ImportFollowState.Preview -> {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
TextButton(onClick = onSkip) { Text("Skip") } TextButton(onClick = onSkip) { Text(stringResource(R.string.skip)) }
Row { Row {
OutlinedButton(onClick = onSearchAnother, shape = RoundedCornerShape(12.dp)) { OutlinedButton(onClick = onSearchAnother, shape = RoundedCornerShape(12.dp)) {
Text("Search Another") Text(stringResource(R.string.search_another))
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Button(onClick = onApply, enabled = state.selectedCount > 0, shape = RoundedCornerShape(12.dp)) { Button(onClick = onApply, enabled = state.selectedCount > 0, shape = RoundedCornerShape(12.dp)) {
Text("Follow ${state.selectedCount} accounts") Text(stringResource(R.string.follow_accounts, state.selectedCount))
} }
} }
} }
@@ -515,15 +538,15 @@ private fun BottomActions(
is ImportFollowState.Done -> { is ImportFollowState.Done -> {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
OutlinedButton(onClick = onSearchAnother, shape = RoundedCornerShape(12.dp)) { OutlinedButton(onClick = onSearchAnother, shape = RoundedCornerShape(12.dp)) {
Text("Import More") Text(stringResource(R.string.import_more))
} }
Button(onClick = onDone, shape = RoundedCornerShape(12.dp)) { Text("Continue") } Button(onClick = onDone, shape = RoundedCornerShape(12.dp)) { Text(stringResource(R.string.continue_txt)) }
} }
} }
is ImportFollowState.Idle, is ImportFollowState.Error -> { is ImportFollowState.Idle, is ImportFollowState.Error -> {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onSkip) { Text("Skip for now") } TextButton(onClick = onSkip) { Text(stringResource(R.string.skip_for_now)) }
} }
} }
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.signup package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
+23
View File
@@ -1229,6 +1229,7 @@
<string name="route_video">Shorts</string> <string name="route_video">Shorts</string>
<string name="route_chess">Chess</string> <string name="route_chess">Chess</string>
<string name="route_security_filters">Security Filters</string> <string name="route_security_filters">Security Filters</string>
<string name="route_import_follows">Import Follows</string>
<string name="new_post">New Post</string> <string name="new_post">New Post</string>
<string name="new_short">New Shorts: images or videos</string> <string name="new_short">New Shorts: images or videos</string>
@@ -1715,4 +1716,26 @@
<string name="kind_voice_msg">Voice Msg</string> <string name="kind_voice_msg">Voice Msg</string>
<string name="kind_voice_reply">Voice Reply</string> <string name="kind_voice_reply">Voice Reply</string>
<string name="kind_wiki">Wiki</string> <string name="kind_wiki">Wiki</string>
<string name="start_with_a_great_feed_by_following_the_same_people_as_someone_you_trust">Start with a great feed by following the same people as someone you trust.</string>
<string name="import_follow_list">Import Follow List</string>
<string name="profile_to_import_from">Profile to import from</string>
<string name="name_search_npub1_alice_example_com">name search, npub1…, alice@example.com</string>
<string name="supports_npub_nip_05_hex_and_namecoin_bit_d_id">Supports npub, NIP-05, hex, and Namecoin (.bit / d/ / id/)</string>
<string name="look_up_follow_list">Look Up Follow List</string>
<string name="tip">Tip</string>
<string name="accounts_found">%1$d accounts found</string>
<string name="selected">%1$d selected</string>
<string name="resolved_via_namecoin">Resolved via Namecoin</string>
<string name="now_following_accounts">Now following %1$d accounts</string>
<string name="your_feed_is_ready">Your feed is ready.</string>
<string name="skip">Skip</string>
<string name="search_another">Search Another</string>
<string name="follow_accounts">Follow %1$d accounts</string>
<string name="import_more">Import More</string>
<string name="continue_txt">Continue</string>
<string name="skip_for_now">Skip for now</string>
<string name="resolving">Resolving %1$s…</string>
<string name="fetching_follow_list">Fetching follow list…</string>
<string name="following_accounts">Following %1$d accounts…</string>
<string name="import_follows_tips">"Enter the profile of a friend or community leader. You can use their npub, NIP-05 address, or a Namecoin name like alice@example.bit or id/alice for blockchain-verified identities."</string>
</resources> </resources>