diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b06386b20..3f6b2493d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -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.PeopleListMetadataScreen 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.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen @@ -177,6 +178,8 @@ fun AppNavigation( composableFromEnd { SettingsScreen(accountViewModel, nav) } composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromEnd { ReactionsSettingsScreen(accountViewModel, nav) } + composableFromEnd { ImportFollowListScreen(accountViewModel, nav) } + composableFromEndArgs { NIP47SetupScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { AllRelayListScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index ef789d612..8b7bcff65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -53,6 +53,8 @@ sealed class Route { @Serializable object BookmarkGroups : Route() + @Serializable object ImportFollows : Route() + @Serializable data class BookmarkGroupView( val dTag: String, val bookmarkType: BookmarkType, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 4756e3fa9..ecae95ec2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -29,27 +29,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst 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.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.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 kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Composable fun AccountScreen(accountSessionManager: AccountSessionManager) { @@ -75,11 +64,7 @@ fun AccountScreen(accountSessionManager: AccountSessionManager) { } is AccountState.LoggedIn -> { - if (state.isNewAccount) { - NewAccountImportFollowsSetup(state.account, accountSessionManager) - } else { - LoggedInSetup(state, accountSessionManager) - } + 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?, - ) { - 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, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt index 5d7379fca..2dafe98a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt @@ -87,7 +87,6 @@ sealed class AccountState { class LoggedIn( val account: Account, var route: Route? = null, - val isNewAccount: Boolean = false, ) : AccountState() } @@ -184,20 +183,10 @@ class AccountSessionManager( fun startUI( accountSettings: AccountSettings, route: Route? = null, - isNewAccount: Boolean = false, ) { val account = accountsCache.loadAccount(accountSettings) _accountContent.update { - AccountState.LoggedIn(account, route, isNewAccount = isNewAccount) - } - } - - fun finishNewAccountSetup() { - val current = _accountContent.value - if (current is AccountState.LoggedIn && current.isNewAccount) { - _accountContent.update { - AccountState.LoggedIn(current.account, current.route, isNewAccount = false) - } + AccountState.LoggedIn(account, route) } } @@ -289,7 +278,7 @@ class AccountSessionManager( localPreferences.setDefaultAccount(accountSettings) - startUI(accountSettings, isNewAccount = true) + startUI(accountSettings, route = Route.ImportFollows) scope.launch(Dispatchers.IO) { delay(2000) // waits for the new user to connect to the new relays. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/signup/ImportFollowListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListScreen.kt similarity index 82% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/signup/ImportFollowListScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListScreen.kt index ebf81762a..67ab33497 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/signup/ImportFollowListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListScreen.kt @@ -18,7 +18,7 @@ * 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.amethyst.ui.screen.signup +package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser import androidx.compose.animation.AnimatedVisibility 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.outlined.Circle import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider @@ -59,6 +57,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -72,20 +71,59 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R 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 fun ImportFollowListSection( onFollowsApplied: suspend (List) -> Unit, @@ -117,11 +155,11 @@ fun ImportFollowListSection( } is ImportFollowState.Resolving -> { - LoadingIndicator("Resolving ${s.identifier}…") + LoadingIndicator(stringResource(R.string.resolving, s.identifier)) } is ImportFollowState.Fetching -> { - LoadingIndicator("Fetching follow list…") + LoadingIndicator(stringResource(R.string.fetching_follow_list)) } is ImportFollowState.Preview -> { @@ -133,7 +171,7 @@ fun ImportFollowListSection( } is ImportFollowState.Applying -> { - LoadingIndicator("Following ${s.count} accounts…") + LoadingIndicator(stringResource(R.string.following_accounts, s.count)) } 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) -> 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 ─────────────────────────────────────────────── @Composable @@ -189,14 +203,14 @@ private fun ImportHeader() { ) Spacer(Modifier.width(10.dp)) Text( - "Import Follow List", + stringResource(R.string.import_follow_list), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, ) } Spacer(Modifier.height(8.dp)) 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, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -215,15 +229,15 @@ private fun InputSection( OutlinedTextField( value = identifier, onValueChange = { identifier = it }, - label = { Text("Profile to import from") }, - placeholder = { Text("npub1…, alice@example.com, or example.bit") }, + label = { Text(stringResource(R.string.profile_to_import_from)) }, + placeholder = { Text(stringResource(R.string.name_search_npub1_alice_example_com)) }, singleLine = true, enabled = enabled, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), supportingText = { 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, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), ) @@ -244,7 +258,7 @@ private fun InputSection( enabled = enabled && identifier.isNotBlank(), modifier = Modifier.fillMaxWidth(), 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) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(24.dp)) { Text( - "Tip", + stringResource(R.string.tip), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.height(4.dp)) Text( - "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.", + stringResource(R.string.import_follows_tips), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -303,17 +315,19 @@ private fun PreviewList( // Summary Row( - Modifier.fillMaxWidth().padding(vertical = 4.dp), + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( - "${state.totalCount} accounts found", + stringResource(R.string.accounts_found, state.totalCount), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, ) Text( - "${state.selectedCount} selected", + stringResource(R.string.selected, state.selectedCount), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary, ) @@ -321,7 +335,10 @@ private fun PreviewList( // Select all 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, ) { Checkbox( @@ -362,7 +379,7 @@ private fun NamecoinResolvedBadge(namecoinSource: String) { Spacer(Modifier.width(8.dp)) Column { Text( - "Resolved via Namecoin", + stringResource(R.string.resolved_via_namecoin), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = Color(0xFF4A90D9), @@ -383,7 +400,10 @@ private fun FollowEntryRow( onToggle: () -> Unit, ) { 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, ) { Icon( @@ -399,7 +419,10 @@ private fun FollowEntryRow( ) Spacer(Modifier.width(10.dp)) Box( - Modifier.size(32.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer), + Modifier + .size(32.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center, ) { Text( @@ -455,13 +478,13 @@ private fun DoneMessage( ) Spacer(Modifier.height(12.dp)) Text( - "Now following $count accounts", + stringResource(R.string.now_following_accounts, count), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, ) Spacer(Modifier.height(6.dp)) Text( - "Your feed is ready.", + stringResource(R.string.your_feed_is_ready), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -499,14 +522,14 @@ private fun BottomActions( when (state) { is ImportFollowState.Preview -> { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - TextButton(onClick = onSkip) { Text("Skip") } + TextButton(onClick = onSkip) { Text(stringResource(R.string.skip)) } Row { OutlinedButton(onClick = onSearchAnother, shape = RoundedCornerShape(12.dp)) { - Text("Search Another") + Text(stringResource(R.string.search_another)) } Spacer(Modifier.width(8.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 -> { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { 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 -> { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - TextButton(onClick = onSkip) { Text("Skip for now") } + TextButton(onClick = onSkip) { Text(stringResource(R.string.skip_for_now)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/signup/ImportFollowListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/signup/ImportFollowListViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListViewModel.kt index eddca8eb7..6b97fdc65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/signup/ImportFollowListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListViewModel.kt @@ -18,7 +18,7 @@ * 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.amethyst.ui.screen.signup +package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 5ffd1b4e5..53b7e4f6e 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1229,6 +1229,7 @@ Shorts Chess Security Filters + Import Follows New Post New Shorts: images or videos @@ -1715,4 +1716,26 @@ Voice Msg Voice Reply Wiki + Start with a great feed by following the same people as someone you trust. + Import Follow List + Profile to import from + name search, npub1…, alice@example.com + Supports npub, NIP-05, hex, and Namecoin (.bit / d/ / id/) + Look Up Follow List + Tip + %1$d accounts found + %1$d selected + Resolved via Namecoin + Now following %1$d accounts + Your feed is ready. + Skip + Search Another + Follow %1$d accounts + Import More + Continue + Skip for now + Resolving %1$s… + Fetching follow list… + Following %1$d accounts… + "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."