Add account switch bottom sheet and update LocalPrefs

This commit is contained in:
maxmoney21m
2023-03-11 23:13:04 +08:00
parent 907aa7b734
commit b7f8241a08
7 changed files with 384 additions and 110 deletions
@@ -11,21 +11,44 @@ import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.toHex import nostr.postr.toHex
import nostr.postr.toNpub
import java.util.Locale import java.util.Locale
data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?)
class LocalPreferences(context: Context) { 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 { private object PrefKeys {
const val NOSTR_PRIVKEY = "nostr_privkey" const val CURRENT_ACCOUNT = "currentlyLoggedInAccount"
const val NOSTR_PUBKEY = "nostr_pubkey"
const val FOLLOWING_CHANNELS = "following_channels" // val NOSTR_PRIVKEY = "nostr_privkey"
const val HIDDEN_USERS = "hidden_users" // val NOSTR_PUBKEY = "nostr_pubkey"
const val RELAYS = "relays" // val FOLLOWING_CHANNELS = "following_channels"
const val DONT_TRANSLATE_FROM = "dontTranslateFrom" // val HIDDEN_USERS = "hidden_users"
const val LANGUAGE_PREFS = "languagePreferences" // val RELAYS = "relays"
const val TRANSLATE_TO = "translateTo" // val DONT_TRANSLATE_FROM = "dontTranslateFrom"
const val ZAP_AMOUNTS = "zapAmounts" // val LANGUAGE_PREFS = "languagePreferences"
const val LATEST_CONTACT_LIST = "latestContactList" // val TRANSLATE_TO = "translateTo"
const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" // 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" } val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
} }
@@ -34,47 +57,88 @@ class LocalPreferences(context: Context) {
fun clearEncryptedStorage() { fun clearEncryptedStorage() {
encryptedPreferences.edit().apply { 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() }.apply()
} }
fun findAllLocalAccounts(): List<AccountInfo> {
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) { fun saveToEncryptedStorage(account: Account) {
val npub = account.loggedIn.pubKey.toNpub()
val myPrefs = prefKeysForAccount(npub)
encryptedPreferences.edit().apply { encryptedPreferences.edit().apply {
account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) } putString(PrefKeys.CURRENT_ACCOUNT, npub)
account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) } account.loggedIn.privKey?.let { putString(myPrefs.NOSTR_PRIVKEY, it.toHex()) }
account.followingChannels.let { putStringSet(PrefKeys.FOLLOWING_CHANNELS, it) } account.loggedIn.pubKey.let { putString(myPrefs.NOSTR_PUBKEY, it.toHex()) }
account.hiddenUsers.let { putStringSet(PrefKeys.HIDDEN_USERS, it) } putStringSet(myPrefs.FOLLOWING_CHANNELS, account.followingChannels)
account.localRelays.let { putString(PrefKeys.RELAYS, gson.toJson(it)) } putStringSet(myPrefs.HIDDEN_USERS, account.hiddenUsers)
account.dontTranslateFrom.let { putStringSet(PrefKeys.DONT_TRANSLATE_FROM, it) } putString(myPrefs.RELAYS, gson.toJson(account.localRelays))
account.languagePreferences.let { putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(it)) } putStringSet(myPrefs.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
account.translateTo.let { putString(PrefKeys.TRANSLATE_TO, it) } putString(myPrefs.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
account.zapAmountChoices.let { putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(it)) } putString(myPrefs.TRANSLATE_TO, account.translateTo)
account.backupContactList.let { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(it)) } putString(myPrefs.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) 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() }.apply()
} }
fun loadFromEncryptedStorage(): Account? { fun loadFromEncryptedStorage(): Account? {
encryptedPreferences.apply { encryptedPreferences.apply {
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) val npub = getString(PrefKeys.CURRENT_ACCOUNT, null) ?: return null
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) val myPrefs = prefKeysForAccount(npub)
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf() 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( val localRelays = gson.fromJson(
getString(PrefKeys.RELAYS, "[]"), getString(myPrefs.RELAYS, "[]"),
object : TypeToken<Set<RelaySetupInfo>>() {}.type object : TypeToken<Set<RelaySetupInfo>>() {}.type
) ?: setOf<RelaySetupInfo>() ) ?: setOf<RelaySetupInfo>()
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() val dontTranslateFrom = getStringSet(myPrefs.DONT_TRANSLATE_FROM, null) ?: setOf()
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language val translateTo = getString(myPrefs.TRANSLATE_TO, null) ?: Locale.getDefault().language
val zapAmountChoices = gson.fromJson( val zapAmountChoices = gson.fromJson(
getString(PrefKeys.ZAP_AMOUNTS, "[]"), getString(myPrefs.ZAP_AMOUNTS, "[]"),
object : TypeToken<List<Long>>() {}.type object : TypeToken<List<Long>>() {}.type
) ?: listOf(500L, 1000L, 5000L) ) ?: listOf(500L, 1000L, 5000L)
val latestContactList = try { 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 Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -83,32 +147,28 @@ class LocalPreferences(context: Context) {
} }
val languagePreferences = try { val languagePreferences = try {
getString(PrefKeys.LANGUAGE_PREFS, null)?.let { getString(myPrefs.LANGUAGE_PREFS, null)?.let {
gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type) as Map<String, String> gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type) as Map<String, String>
} ?: mapOf<String, String>() } ?: mapOf()
} catch (e: Throwable) { } catch (e: Throwable) {
e.printStackTrace() e.printStackTrace()
mapOf<String, String>() mapOf()
} }
val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) val hideDeleteRequestInfo = getBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, false)
if (pubKey != null) { return Account(
return Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), followingChannels,
followingChannels, hiddenUsers,
hiddenUsers, localRelays,
localRelays, dontTranslateFrom,
dontTranslateFrom, languagePreferences,
languagePreferences, translateTo,
translateTo, zapAmountChoices,
zapAmountChoices, hideDeleteRequestInfo,
hideDeleteRequestInfo, latestContactList
latestContactList )
)
} else {
return null
}
} }
} }
@@ -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")
}
}
}
}
@@ -16,9 +16,11 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ScaffoldState import androidx.compose.material.ScaffoldState
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text 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 com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun DrawerContent( fun DrawerContent(
navController: NavHostController, navController: NavHostController,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
sheetState: ModalBottomSheetState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel accountStateViewModel: AccountStateViewModel
) { ) {
@@ -88,6 +92,7 @@ fun DrawerContent(
account.userProfile(), account.userProfile(),
navController, navController,
scaffoldState, scaffoldState,
sheetState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1F), .weight(1F),
@@ -214,15 +219,18 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
} }
} }
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun ListContent( fun ListContent(
accountUser: User?, accountUser: User?,
navController: NavHostController, navController: NavHostController,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
sheetState: ModalBottomSheetState,
modifier: Modifier, modifier: Modifier,
accountViewModel: AccountStateViewModel, accountViewModel: AccountStateViewModel,
account: Account account: Account
) { ) {
val coroutineScope = rememberCoroutineScope()
var backupDialogOpen by remember { mutableStateOf(false) } var backupDialogOpen by remember { mutableStateOf(false) }
Column(modifier = modifier.fillMaxHeight()) { Column(modifier = modifier.fillMaxHeight()) {
@@ -260,11 +268,18 @@ fun ListContent(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconRow( IconRow(
stringResource(R.string.log_out), title = "Accounts",
R.drawable.ic_logout, icon = R.drawable.manage_accounts,
MaterialTheme.colors.onBackground, tint = MaterialTheme.colors.onBackground,
onClick = { accountViewModel.logOff() } 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) { if (backupDialogOpen) {
@@ -36,62 +36,95 @@ sealed class Route(
val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit
) { ) {
object Home : Route( object Home : Route(
"Home", route = "Home",
R.drawable.ic_home, icon = R.drawable.ic_home,
hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) }, hasNewItems = { accountViewModel, cache, context ->
buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } } homeHasNewItems(accountViewModel, cache, context)
},
buildScreen = { accountViewModel, _, navController ->
{ HomeScreen(accountViewModel, navController) }
}
) )
object Search : Route( object Search : Route(
"Search", route = "Search",
R.drawable.ic_globe, icon = R.drawable.ic_globe,
buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) } } buildScreen = { accountViewModel, _, navController ->
{ SearchScreen(accountViewModel, navController) }
}
) )
object Notification : Route( object Notification : Route(
"Notification", route = "Notification",
R.drawable.ic_notifications, icon = R.drawable.ic_notifications,
hasNewItems = { acc, cache, ctx -> notificationHasNewItems(acc, cache, ctx) }, hasNewItems = { accountViewModel, cache, context ->
buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) } } notificationHasNewItems(accountViewModel, cache, context)
},
buildScreen = { accountViewModel, _, navController ->
{ NotificationScreen(accountViewModel, navController) }
}
) )
object Message : Route( object Message : Route(
"Message", route = "Message",
R.drawable.ic_dm, icon = R.drawable.ic_dm,
hasNewItems = { acc, cache, ctx -> messagesHasNewItems(acc, cache, ctx) }, hasNewItems = { accountViewModel, cache, context ->
buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) } } messagesHasNewItems(accountViewModel, cache, context)
},
buildScreen = { accountViewModel, _, navController ->
{ ChatroomListScreen(accountViewModel, navController) }
}
) )
object Filters : Route( object Filters : Route(
"Filters", route = "Filters",
R.drawable.ic_security, icon = R.drawable.ic_security,
buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) } } buildScreen = { accountViewModel, _, navController ->
{ FiltersScreen(accountViewModel, navController) }
}
) )
object Profile : Route( object Profile : Route(
"User/{id}", route = "User/{id}",
R.drawable.ic_profile, icon = R.drawable.ic_profile,
arguments = listOf(navArgument("id") { type = NavType.StringType }), 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( object Note : Route(
"Note/{id}", route = "Note/{id}",
R.drawable.ic_moments, icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType }), 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( object Room : Route(
"Room/{id}", route = "Room/{id}",
R.drawable.ic_moments, icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType }), 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( object Channel : Route(
"Channel/{id}", route = "Channel/{id}",
R.drawable.ic_moments, icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType }), 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 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) val lastTime = cache.load("Notification", context)
NotificationFeedFilter.account = account 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 ChatroomListKnownFeedFilter.account = account
val note = ChatroomListKnownFeedFilter.feed().firstOrNull { val note = ChatroomListKnownFeedFilter.feed().firstOrNull {
@@ -78,7 +78,12 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
@Composable @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 accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account val account = accountState?.account
@@ -7,9 +7,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.DrawerValue import androidx.compose.material.DrawerValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.rememberDrawerState import androidx.compose.material.rememberDrawerState
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material.rememberScaffoldState import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -19,6 +23,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.buttons.NewChannelButton import com.vitorpamplona.amethyst.buttons.NewChannelButton
import com.vitorpamplona.amethyst.buttons.NewNoteButton 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.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
import com.vitorpamplona.amethyst.ui.navigation.AppTopBar 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.AccountState
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) {
val navController = rememberNavController() val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded },
skipHalfExpanded = true
)
Scaffold( ModalBottomSheetLayout(
modifier = Modifier sheetState = sheetState,
.background(MaterialTheme.colors.primaryVariant) sheetContent = {
.statusBarsPadding(), AccountSwitchBottomSheet(accountViewModel = accountViewModel, sheetState = sheetState)
bottomBar = { }
AppBottomBar(navController, accountViewModel)
},
topBar = {
AppTopBar(navController, scaffoldState, accountViewModel)
},
drawerContent = {
DrawerContent(navController, scaffoldState, accountViewModel, accountStateViewModel)
},
floatingActionButton = {
FloatingButton(navController, accountStateViewModel)
},
scaffoldState = scaffoldState
) { ) {
Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { Scaffold(
AppNavigation(navController, accountViewModel, accountStateViewModel, startingPage) 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)
}
} }
} }
} }
@@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
<path android:fillColor="@android:color/white" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
<path android:fillColor="@android:color/white" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
</vector>