diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 83e0f331a4..bbe4e24d2f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -11,21 +11,44 @@ import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import nostr.postr.Persona import nostr.postr.toHex +import nostr.postr.toNpub import java.util.Locale +data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?) + class LocalPreferences(context: Context) { + + private fun prefKeysForAccount(npub: String) = object { + val NOSTR_PRIVKEY = "$npub/nostr_privkey" + val NOSTR_PUBKEY = "$npub/nostr_pubkey" + val DISPLAY_NAME = "$npub/display_name" + val PROFILE_PICTURE_URL = "$npub/profile_picture" + val FOLLOWING_CHANNELS = "$npub/following_channels" + val HIDDEN_USERS = "$npub/hidden_users" + val RELAYS = "$npub/relays" + val DONT_TRANSLATE_FROM = "$npub/dontTranslateFrom" + val LANGUAGE_PREFS = "$npub/languagePreferences" + val TRANSLATE_TO = "$npub/translateTo" + val ZAP_AMOUNTS = "$npub/zapAmounts" + val LATEST_CONTACT_LIST = "$npub/latestContactList" + val HIDE_DELETE_REQUEST_INFO = "$npub/hideDeleteRequestInfo" +// val LAST_READ: (String) -> String = { route -> "$npub/last_read_route_$route" } + } + private object PrefKeys { - const val NOSTR_PRIVKEY = "nostr_privkey" - const val NOSTR_PUBKEY = "nostr_pubkey" - const val FOLLOWING_CHANNELS = "following_channels" - const val HIDDEN_USERS = "hidden_users" - const val RELAYS = "relays" - const val DONT_TRANSLATE_FROM = "dontTranslateFrom" - const val LANGUAGE_PREFS = "languagePreferences" - const val TRANSLATE_TO = "translateTo" - const val ZAP_AMOUNTS = "zapAmounts" - const val LATEST_CONTACT_LIST = "latestContactList" - const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" + const val CURRENT_ACCOUNT = "currentlyLoggedInAccount" + +// val NOSTR_PRIVKEY = "nostr_privkey" +// val NOSTR_PUBKEY = "nostr_pubkey" +// val FOLLOWING_CHANNELS = "following_channels" +// val HIDDEN_USERS = "hidden_users" +// val RELAYS = "relays" +// val DONT_TRANSLATE_FROM = "dontTranslateFrom" +// val LANGUAGE_PREFS = "languagePreferences" +// val TRANSLATE_TO = "translateTo" +// val ZAP_AMOUNTS = "zapAmounts" +// val LATEST_CONTACT_LIST = "latestContactList" +// val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -34,47 +57,88 @@ class LocalPreferences(context: Context) { fun clearEncryptedStorage() { encryptedPreferences.edit().apply { - encryptedPreferences.all.keys.forEach { remove(it) } + encryptedPreferences.all.keys.forEach { + remove(it) + } +// encryptedPreferences.all.keys.filter { +// it.startsWith(npub) +// }.forEach { +// remove(it) +// } }.apply() } + fun findAllLocalAccounts(): List { + encryptedPreferences.apply { + val currentAccount = getString(PrefKeys.CURRENT_ACCOUNT, null) + return encryptedPreferences.all.keys.filter { + it.endsWith("nostr_pubkey") + }.map { + val npub = it.substringBefore("/") + val myPrefs = prefKeysForAccount(npub) + AccountInfo( + npub, + npub == currentAccount, + getString(myPrefs.DISPLAY_NAME, null), + getString(myPrefs.PROFILE_PICTURE_URL, null) + ) + } + } + } + fun saveToEncryptedStorage(account: Account) { + val npub = account.loggedIn.pubKey.toNpub() + val myPrefs = prefKeysForAccount(npub) + encryptedPreferences.edit().apply { - account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) } - account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) } - account.followingChannels.let { putStringSet(PrefKeys.FOLLOWING_CHANNELS, it) } - account.hiddenUsers.let { putStringSet(PrefKeys.HIDDEN_USERS, it) } - account.localRelays.let { putString(PrefKeys.RELAYS, gson.toJson(it)) } - account.dontTranslateFrom.let { putStringSet(PrefKeys.DONT_TRANSLATE_FROM, it) } - account.languagePreferences.let { putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(it)) } - account.translateTo.let { putString(PrefKeys.TRANSLATE_TO, it) } - account.zapAmountChoices.let { putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(it)) } - account.backupContactList.let { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(it)) } - putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) + putString(PrefKeys.CURRENT_ACCOUNT, npub) + account.loggedIn.privKey?.let { putString(myPrefs.NOSTR_PRIVKEY, it.toHex()) } + account.loggedIn.pubKey.let { putString(myPrefs.NOSTR_PUBKEY, it.toHex()) } + putStringSet(myPrefs.FOLLOWING_CHANNELS, account.followingChannels) + putStringSet(myPrefs.HIDDEN_USERS, account.hiddenUsers) + putString(myPrefs.RELAYS, gson.toJson(account.localRelays)) + putStringSet(myPrefs.DONT_TRANSLATE_FROM, account.dontTranslateFrom) + putString(myPrefs.LANGUAGE_PREFS, gson.toJson(account.languagePreferences)) + putString(myPrefs.TRANSLATE_TO, account.translateTo) + putString(myPrefs.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) + putString(myPrefs.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) + putBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) + }.apply() + } + + fun saveCurrentAccountMetadata(account: Account) { + val myPrefs = prefKeysForAccount(account.loggedIn.pubKey.toNpub()) + + encryptedPreferences.edit().apply { + putString(myPrefs.DISPLAY_NAME, account.userProfile().toBestDisplayName()) + putString(myPrefs.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) }.apply() } fun loadFromEncryptedStorage(): Account? { encryptedPreferences.apply { - val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) - val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) - val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() - val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf() + val npub = getString(PrefKeys.CURRENT_ACCOUNT, null) ?: return null + val myPrefs = prefKeysForAccount(npub) + + val pubKey = getString(myPrefs.NOSTR_PUBKEY, null) ?: return null + val privKey = getString(myPrefs.NOSTR_PRIVKEY, null) + val followingChannels = getStringSet(myPrefs.FOLLOWING_CHANNELS, null) ?: setOf() + val hiddenUsers = getStringSet(myPrefs.HIDDEN_USERS, emptySet()) ?: setOf() val localRelays = gson.fromJson( - getString(PrefKeys.RELAYS, "[]"), + getString(myPrefs.RELAYS, "[]"), object : TypeToken>() {}.type ) ?: setOf() - val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() - val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language + val dontTranslateFrom = getStringSet(myPrefs.DONT_TRANSLATE_FROM, null) ?: setOf() + val translateTo = getString(myPrefs.TRANSLATE_TO, null) ?: Locale.getDefault().language val zapAmountChoices = gson.fromJson( - getString(PrefKeys.ZAP_AMOUNTS, "[]"), + getString(myPrefs.ZAP_AMOUNTS, "[]"), object : TypeToken>() {}.type ) ?: listOf(500L, 1000L, 5000L) val latestContactList = try { - getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { + getString(myPrefs.LATEST_CONTACT_LIST, null)?.let { Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent } } catch (e: Throwable) { @@ -83,32 +147,28 @@ class LocalPreferences(context: Context) { } val languagePreferences = try { - getString(PrefKeys.LANGUAGE_PREFS, null)?.let { + getString(myPrefs.LANGUAGE_PREFS, null)?.let { gson.fromJson(it, object : TypeToken>() {}.type) as Map - } ?: mapOf() + } ?: mapOf() } catch (e: Throwable) { e.printStackTrace() - mapOf() + mapOf() } - val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) + val hideDeleteRequestInfo = getBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, false) - if (pubKey != null) { - return Account( - Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), - followingChannels, - hiddenUsers, - localRelays, - dontTranslateFrom, - languagePreferences, - translateTo, - zapAmountChoices, - hideDeleteRequestInfo, - latestContactList - ) - } else { - return null - } + return Account( + Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), + followingChannels, + hiddenUsers, + localRelays, + dontTranslateFrom, + languagePreferences, + translateTo, + zapAmountChoices, + hideDeleteRequestInfo, + latestContactList + ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt new file mode 100644 index 0000000000..f206ac2d03 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -0,0 +1,122 @@ +package com.vitorpamplona.amethyst.ui.navigation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Logout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.RoboHashCache +import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.note.toShortenHex +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AccountSwitchBottomSheet( + accountViewModel: AccountViewModel, + sheetState: ModalBottomSheetState +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val localPrefs = LocalPreferences(context) + val accounts = localPrefs.findAllLocalAccounts() + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val accountUserState by account.userProfile().live().metadata.observeAsState() + val accountUser = accountUserState?.user ?: return + + LaunchedEffect(key1 = accountUser) { + localPrefs.saveCurrentAccountMetadata(account) + } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Select Account", fontWeight = FontWeight.Bold) + } + accounts.forEach { acc -> + val current = accountUser.pubkeyNpub() == acc.npub + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp, 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImageProxy( + model = ResizeImage(acc.profilePicture, 64.dp), + placeholder = BitmapPainter(RoboHashCache.get(context, acc.npub)), + fallback = BitmapPainter(RoboHashCache.get(context, acc.npub)), + error = BitmapPainter(RoboHashCache.get(context, acc.npub)), + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(64.dp) + .height(64.dp) + .clip(shape = CircleShape) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + acc.displayName?.let { + Text(it) + } + Text(acc.npub.toShortenHex()) + } + Spacer(modifier = Modifier.width(8.dp)) + if (current) { + Text("✓") + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { /*TODO*/ }) { + Icon(imageVector = Icons.Default.Logout, "Logout") + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton(onClick = { coroutineScope.launch { sheetState.hide() } }) { + Text("Add New Account") + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 9ac1cb2c46..8198b70581 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -16,9 +16,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ScaffoldState import androidx.compose.material.Surface import androidx.compose.material.Text @@ -56,10 +58,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterialApi::class) @Composable fun DrawerContent( navController: NavHostController, scaffoldState: ScaffoldState, + sheetState: ModalBottomSheetState, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel ) { @@ -88,6 +92,7 @@ fun DrawerContent( account.userProfile(), navController, scaffoldState, + sheetState, modifier = Modifier .fillMaxWidth() .weight(1F), @@ -214,15 +219,18 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun ListContent( accountUser: User?, navController: NavHostController, scaffoldState: ScaffoldState, + sheetState: ModalBottomSheetState, modifier: Modifier, accountViewModel: AccountStateViewModel, account: Account ) { + val coroutineScope = rememberCoroutineScope() var backupDialogOpen by remember { mutableStateOf(false) } Column(modifier = modifier.fillMaxHeight()) { @@ -260,11 +268,18 @@ fun ListContent( Spacer(modifier = Modifier.weight(1f)) IconRow( - stringResource(R.string.log_out), - R.drawable.ic_logout, - MaterialTheme.colors.onBackground, - onClick = { accountViewModel.logOff() } + title = "Accounts", + icon = R.drawable.manage_accounts, + tint = MaterialTheme.colors.onBackground, + onClick = { coroutineScope.launch { sheetState.show() } } ) + +// IconRow( +// title = stringResource(R.string.log_out), +// icon = R.drawable.ic_logout, +// tint = MaterialTheme.colors.onBackground, +// onClick = { accountViewModel.logOff() } +// ) } if (backupDialogOpen) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 66bed57fac..8ca43e4e1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -36,62 +36,95 @@ sealed class Route( val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { object Home : Route( - "Home", - R.drawable.ic_home, - hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } } + route = "Home", + icon = R.drawable.ic_home, + hasNewItems = { accountViewModel, cache, context -> + homeHasNewItems(accountViewModel, cache, context) + }, + buildScreen = { accountViewModel, _, navController -> + { HomeScreen(accountViewModel, navController) } + } ) + object Search : Route( - "Search", - R.drawable.ic_globe, - buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) } } + route = "Search", + icon = R.drawable.ic_globe, + buildScreen = { accountViewModel, _, navController -> + { SearchScreen(accountViewModel, navController) } + } ) + object Notification : Route( - "Notification", - R.drawable.ic_notifications, - hasNewItems = { acc, cache, ctx -> notificationHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) } } + route = "Notification", + icon = R.drawable.ic_notifications, + hasNewItems = { accountViewModel, cache, context -> + notificationHasNewItems(accountViewModel, cache, context) + }, + buildScreen = { accountViewModel, _, navController -> + { NotificationScreen(accountViewModel, navController) } + } ) object Message : Route( - "Message", - R.drawable.ic_dm, - hasNewItems = { acc, cache, ctx -> messagesHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) } } + route = "Message", + icon = R.drawable.ic_dm, + hasNewItems = { accountViewModel, cache, context -> + messagesHasNewItems(accountViewModel, cache, context) + }, + buildScreen = { accountViewModel, _, navController -> + { ChatroomListScreen(accountViewModel, navController) } + } ) object Filters : Route( - "Filters", - R.drawable.ic_security, - buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) } } + route = "Filters", + icon = R.drawable.ic_security, + buildScreen = { accountViewModel, _, navController -> + { FiltersScreen(accountViewModel, navController) } + } ) object Profile : Route( - "User/{id}", - R.drawable.ic_profile, + route = "User/{id}", + icon = R.drawable.ic_profile, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ProfileScreen(it.arguments?.getString("id"), acc, nav) } } + buildScreen = { accountViewModel, _, navController -> + { ProfileScreen(it.arguments?.getString("id"), accountViewModel, navController) } + } ) object Note : Route( - "Note/{id}", - R.drawable.ic_moments, + route = "Note/{id}", + icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) } } + buildScreen = { accountViewModel, _, navController -> + { ThreadScreen(it.arguments?.getString("id"), accountViewModel, navController) } + } ) object Room : Route( - "Room/{id}", - R.drawable.ic_moments, + route = "Room/{id}", + icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ChatroomScreen(it.arguments?.getString("id"), acc, nav) } } + buildScreen = { accountViewModel, _, navController -> + { ChatroomScreen(it.arguments?.getString("id"), accountViewModel, navController) } + } ) object Channel : Route( - "Channel/{id}", - R.drawable.ic_moments, + route = "Channel/{id}", + icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ChannelScreen(it.arguments?.getString("id"), acc, accSt, nav) } } + buildScreen = { accountViewModel, accountStateViewModel, navController -> + { + ChannelScreen( + it.arguments?.getString("id"), + accountViewModel, + accountStateViewModel, + navController + ) + } + } ) } @@ -124,18 +157,32 @@ private fun homeHasNewItems(account: Account, cache: NotificationCache, context: HomeNewThreadFeedFilter.account = account - return (HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime + return ( + HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() + ?: 0 + ) > lastTime } -private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { +private fun notificationHasNewItems( + account: Account, + cache: NotificationCache, + context: Context +): Boolean { val lastTime = cache.load("Notification", context) NotificationFeedFilter.account = account - return (NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime + return ( + NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() + ?: 0 + ) > lastTime } -private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { +private fun messagesHasNewItems( + account: Account, + cache: NotificationCache, + context: Context +): Boolean { ChatroomListKnownFeedFilter.account = account val note = ChatroomListKnownFeedFilter.feed().firstOrNull { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 71fa2ee6cc..496666f7e3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -78,7 +78,12 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel @Composable -fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, navController: NavController) { +fun ChannelScreen( + channelId: String?, + accountViewModel: AccountViewModel, + accountStateViewModel: AccountStateViewModel, + navController: NavController +) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index fa698b2759..e296fec908 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -7,9 +7,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.DrawerValue +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Scaffold import androidx.compose.material.rememberDrawerState +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -19,6 +23,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.vitorpamplona.amethyst.buttons.NewChannelButton import com.vitorpamplona.amethyst.buttons.NewNoteButton +import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.AppTopBar @@ -28,31 +33,44 @@ import com.vitorpamplona.amethyst.ui.navigation.currentRoute import com.vitorpamplona.amethyst.ui.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { val navController = rememberNavController() val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded }, + skipHalfExpanded = true + ) - Scaffold( - modifier = Modifier - .background(MaterialTheme.colors.primaryVariant) - .statusBarsPadding(), - bottomBar = { - AppBottomBar(navController, accountViewModel) - }, - topBar = { - AppTopBar(navController, scaffoldState, accountViewModel) - }, - drawerContent = { - DrawerContent(navController, scaffoldState, accountViewModel, accountStateViewModel) - }, - floatingActionButton = { - FloatingButton(navController, accountStateViewModel) - }, - scaffoldState = scaffoldState + ModalBottomSheetLayout( + sheetState = sheetState, + sheetContent = { + AccountSwitchBottomSheet(accountViewModel = accountViewModel, sheetState = sheetState) + } ) { - Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { - AppNavigation(navController, accountViewModel, accountStateViewModel, startingPage) + Scaffold( + modifier = Modifier + .background(MaterialTheme.colors.primaryVariant) + .statusBarsPadding(), + bottomBar = { + AppBottomBar(navController, accountViewModel) + }, + topBar = { + AppTopBar(navController, scaffoldState, accountViewModel) + }, + drawerContent = { + DrawerContent(navController, scaffoldState, sheetState, accountViewModel, accountStateViewModel) + }, + floatingActionButton = { + FloatingButton(navController, accountStateViewModel) + }, + scaffoldState = scaffoldState + ) { + Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { + AppNavigation(navController, accountViewModel, accountStateViewModel, startingPage) + } } } } diff --git a/app/src/main/res/drawable/manage_accounts.xml b/app/src/main/res/drawable/manage_accounts.xml new file mode 100644 index 0000000000..734c705597 --- /dev/null +++ b/app/src/main/res/drawable/manage_accounts.xml @@ -0,0 +1,7 @@ + + + + +