From 8d7897601b5b9841a60bf517c5bf59bb115c29ec Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 3 Mar 2023 02:24:13 +0800 Subject: [PATCH 1/8] Add buttons to mark DMs and chatrooms as read #172 --- .../ui/screen/ChatroomListFeedView.kt | 60 ++++++- .../ui/screen/loggedIn/AccountViewModel.kt | 3 - .../ui/screen/loggedIn/ChatroomListScreen.kt | 158 +++++++++++++----- app/src/main/res/values/strings.xml | 3 + 4 files changed, 174 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index ef4a9c386..6a54db451 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -9,21 +9,30 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.ui.note.ChatroomCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { +fun ChatroomListFeedView( + viewModel: FeedViewModel, + accountViewModel: AccountViewModel, + navController: NavController, + markAsRead: MutableState +) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() var isRefreshing by remember { mutableStateOf(false) } @@ -45,21 +54,27 @@ fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountView }, ) { Column() { - Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> + Crossfade( + targetState = feedState, + animationSpec = tween(durationMillis = 100) + ) { state -> when (state) { is FeedState.Empty -> { FeedEmpty { isRefreshing = true } } + is FeedState.FeedError -> { FeedError(state.errorMessage) { isRefreshing = true } } + is FeedState.Loaded -> { - FeedLoaded(state, accountViewModel, navController) + FeedLoaded(state, accountViewModel, navController, markAsRead) } + FeedState.Loading -> { LoadingFeed() } @@ -73,10 +88,43 @@ fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountView private fun FeedLoaded( state: FeedState.Loaded, accountViewModel: AccountViewModel, - navController: NavController + navController: NavController, + markAsRead: MutableState, ) { val listState = rememberLazyListState() + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + val notificationCacheState = NotificationCache.live.observeAsState() + val notificationCache = notificationCacheState.value ?: return + val context = LocalContext.current.applicationContext + + if (markAsRead.value) { + LaunchedEffect(key1 = notificationCache) { + for (note in state.feed.value) { + note.event?.let { + var route = "" + + if (note.channel != null) { + route = "Channel/${note.channel!!.idHex}" + } else { + val replyAuthorBase = note.mentions?.first() + var userToComposeOn = note.author!! + if (replyAuthorBase != null) { + if (note.author == account.userProfile()) { + userToComposeOn = replyAuthorBase + } + } + route = "Room/${userToComposeOn.pubkeyHex}" + } + + notificationCache.cache.markAsRead(route, it.createdAt, context) + } + } + markAsRead.value = false + } + } + LazyColumn( contentPadding = PaddingValues( top = 10.dp, @@ -84,7 +132,9 @@ private fun FeedLoaded( ), state = listState ) { - itemsIndexed(state.feed.value, key = { index, item -> if (index == 0) index else item.idHex }) { index, item -> + itemsIndexed( + state.feed.value, + key = { index, item -> if (index == 0) index else item.idHex }) { index, item -> ChatroomCompose( item, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 66e702358..42687c2d4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -113,7 +113,4 @@ class AccountViewModel(private val account: Account): ViewModel() { account.prefer(source, target, preference) } - - - } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 4572dabd9..387df78ce 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -1,31 +1,42 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Tab import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter @@ -33,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel import kotlinx.coroutines.launch -import com.vitorpamplona.amethyst.R @OptIn(ExperimentalPagerApi::class) @Composable @@ -41,48 +51,78 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon val pagerState = rememberPagerState() val coroutineScope = rememberCoroutineScope() - Column(Modifier.fillMaxHeight()) { - Column( - modifier = Modifier.padding(vertical = 0.dp) - ) { - TabRow( - backgroundColor = MaterialTheme.colors.background, - selectedTabIndex = pagerState.currentPage, - indicator = { tabPositions -> - TabRowDefaults.Indicator( - Modifier.pagerTabIndicatorOffset(pagerState, tabPositions), - color = MaterialTheme.colors.primary - ) - }, - ) { - Tab( - selected = pagerState.currentPage == 0, - onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } }, - text = { - Text(text = stringResource(R.string.known)) - } - ) + var moreActionsExpanded by remember { mutableStateOf(false) } + val markKnownAsRead = remember { mutableStateOf(false) } + val markNewAsRead = remember { mutableStateOf(false) } - Tab( - selected = pagerState.currentPage == 1, - onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, - text = { - Text(text = stringResource(R.string.new_requests)) + Box(Modifier.fillMaxSize()) { + Column(Modifier.fillMaxHeight()) { + Column( + modifier = Modifier.padding(vertical = 0.dp) + ) { + TabRow( + backgroundColor = MaterialTheme.colors.background, + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.pagerTabIndicatorOffset(pagerState, tabPositions), + color = MaterialTheme.colors.primary + ) + }, + ) { + Tab( + selected = pagerState.currentPage == 0, + onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } }, + text = { + Text(text = stringResource(R.string.known)) + } + ) + + Tab( + selected = pagerState.currentPage == 1, + onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, + text = { + Text(text = stringResource(R.string.new_requests)) + } + ) + } + HorizontalPager(count = 2, state = pagerState) { + when (pagerState.currentPage) { + 0 -> TabKnown(accountViewModel, navController, markKnownAsRead,) + 1 -> TabNew(accountViewModel, navController, markNewAsRead) } - ) - } - HorizontalPager(count = 2, state = pagerState) { - when (pagerState.currentPage) { - 0 -> TabKnown(accountViewModel, navController) - 1 -> TabNew(accountViewModel, navController) } } } + IconButton( + modifier = Modifier + .padding(0.dp) + .size(30.dp) + .align(Alignment.TopEnd), + onClick = { moreActionsExpanded = true } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + ) + + ChatroomTabMenu( + moreActionsExpanded, + { moreActionsExpanded = false }, + { markKnownAsRead.value = true }, + { markNewAsRead.value = true }, + ) + } } } @Composable -fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { +fun TabKnown( + accountViewModel: AccountViewModel, + navController: NavController, + markAsRead: MutableState, +) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -98,13 +138,17 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { Column( modifier = Modifier.padding(vertical = 0.dp) ) { - ChatroomListFeedView(feedViewModel, accountViewModel, navController) + ChatroomListFeedView(feedViewModel, accountViewModel, navController, markAsRead) } } } @Composable -fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { +fun TabNew( + accountViewModel: AccountViewModel, + navController: NavController, + markAsRead: MutableState +) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -120,7 +164,37 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { Column( modifier = Modifier.padding(vertical = 0.dp) ) { - ChatroomListFeedView(feedViewModel, accountViewModel, navController) + ChatroomListFeedView(feedViewModel, accountViewModel, navController, markAsRead) } } -} \ No newline at end of file +} + +@Composable +fun ChatroomTabMenu( + expanded: Boolean, + onDismiss: () -> Unit, + onMarkKnownAsRead: () -> Unit, + onMarkNewAsRead: () -> Unit, +) { + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { + DropdownMenuItem(onClick = { + onMarkKnownAsRead() + onDismiss() + }) { + Text(stringResource(R.string.mark_all_known_as_read)) + } + DropdownMenuItem(onClick = { + onMarkNewAsRead() + onDismiss() + }) { + Text(stringResource(R.string.mark_all_new_as_read)) + } + DropdownMenuItem(onClick = { + onMarkKnownAsRead() + onMarkNewAsRead() + onDismiss() + }) { + Text(stringResource(R.string.mark_all_as_read)) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 952cd426c..84cf1d090 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,4 +177,7 @@ Report Hateful speech Report Nudity / Porn others + Mark all Known as read + Mark all New as read + Mark all as read \ No newline at end of file From e07400409f227fff0f15da348726dcd09027a563 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 3 Mar 2023 21:12:15 +0800 Subject: [PATCH 2/8] set clearEncryptedStorage to remove all keys including lastRead timestamps --- .../com/vitorpamplona/amethyst/LocalPreferences.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 8684b11d1..1662ff3d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -19,16 +19,7 @@ class LocalPreferences(context: Context) { fun clearEncryptedStorage() { encryptedPreferences.edit().apply { - remove("nostr_privkey") - remove("nostr_pubkey") - remove("following_channels") - remove("hidden_users") - remove("relays") - remove("dontTranslateFrom") - remove("languagePreferences") - remove("translateTo") - remove("zapAmounts") - remove("latestContactList") + encryptedPreferences.all.keys.forEach { remove(it) } }.apply() } From f34dea868d4fe8464a0ff79140360eebdcc2a541 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 3 Mar 2023 21:17:04 +0800 Subject: [PATCH 3/8] extract LocalPreferences preference keys --- .../amethyst/LocalPreferences.kt | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 1662ff3d0..45c1cdd8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -14,8 +14,22 @@ import nostr.postr.events.Event.Companion.getRefinedEvent import nostr.postr.toHex class LocalPreferences(context: Context) { - val encryptedPreferences = EncryptedStorage().preferences(context) - val gson = GsonBuilder().create() + 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" + val LAST_READ: (String) -> String = { route -> "last_read_route_${route}" } + } + + private val encryptedPreferences = EncryptedStorage().preferences(context) + private val gson = GsonBuilder().create() fun clearEncryptedStorage() { encryptedPreferences.edit().apply { @@ -25,40 +39,40 @@ class LocalPreferences(context: Context) { fun saveToEncryptedStorage(account: Account) { encryptedPreferences.edit().apply { - account.loggedIn.privKey?.let { putString("nostr_privkey", it.toHex()) } - account.loggedIn.pubKey.let { putString("nostr_pubkey", it.toHex()) } - account.followingChannels.let { putStringSet("following_channels", it) } - account.hiddenUsers.let { putStringSet("hidden_users", it) } - account.localRelays.let { putString("relays", gson.toJson(it)) } - account.dontTranslateFrom.let { putStringSet("dontTranslateFrom", it) } - account.languagePreferences.let { putString("languagePreferences", gson.toJson(it)) } - account.translateTo.let { putString("translateTo", it) } - account.zapAmountChoices.let { putString("zapAmounts", gson.toJson(it)) } - account.backupContactList.let { putString("latestContactList", Event.gson.toJson(it)) } + 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)) } }.apply() } fun loadFromEncryptedStorage(): Account? { encryptedPreferences.apply { - val privKey = getString("nostr_privkey", null) - val pubKey = getString("nostr_pubkey", null) - val followingChannels = getStringSet("following_channels", null) ?: setOf() - val hiddenUsers = getStringSet("hidden_users", emptySet()) ?: setOf() + 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 localRelays = gson.fromJson( - getString("relays", "[]"), + getString(PrefKeys.RELAYS, "[]"), object : TypeToken>() {}.type ) ?: setOf() - val dontTranslateFrom = getStringSet("dontTranslateFrom", null) ?: setOf() - val translateTo = getString("translateTo", null) ?: Locale.getDefault().language + val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() + val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language val zapAmountChoices = gson.fromJson( - getString("zapAmounts", "[]"), + getString(PrefKeys.ZAP_AMOUNTS, "[]"), object : TypeToken>() {}.type ) ?: listOf(500L, 1000L, 5000L) val latestContactList = try { - getString("latestContactList", null)?.let { + getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent } } catch (e: Throwable) { @@ -67,7 +81,7 @@ class LocalPreferences(context: Context) { } val languagePreferences = try { - getString("languagePreferences", null)?.let { + getString(PrefKeys.LANGUAGE_PREFS, null)?.let { gson.fromJson(it, object : TypeToken>() {}.type) as Map } ?: mapOf() } catch (e: Throwable) { @@ -95,13 +109,13 @@ class LocalPreferences(context: Context) { fun saveLastRead(route: String, timestampInSecs: Long) { encryptedPreferences.edit().apply { - putLong("last_read_route_${route}", timestampInSecs) + putLong(PrefKeys.LAST_READ(route), timestampInSecs) }.apply() } fun loadLastRead(route: String): Long { encryptedPreferences.run { - return getLong("last_read_route_${route}", 0) + return getLong(PrefKeys.LAST_READ(route), 0) } } From 75901d83a8d37355810023bc45c244ed70210ad4 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 3 Mar 2023 21:17:27 +0800 Subject: [PATCH 4/8] reindent LocalPreferences with 4 spaces --- .../amethyst/LocalPreferences.kt | 186 +++++++++--------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 45c1cdd8d..7e176e193 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -14,109 +14,109 @@ import nostr.postr.events.Event.Companion.getRefinedEvent import nostr.postr.toHex class LocalPreferences(context: Context) { - 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" - val LAST_READ: (String) -> String = { route -> "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" + val LAST_READ: (String) -> String = { route -> "last_read_route_${route}" } + } - private val encryptedPreferences = EncryptedStorage().preferences(context) - private val gson = GsonBuilder().create() + private val encryptedPreferences = EncryptedStorage().preferences(context) + private val gson = GsonBuilder().create() - fun clearEncryptedStorage() { - encryptedPreferences.edit().apply { - encryptedPreferences.all.keys.forEach { remove(it) } - }.apply() - } + fun clearEncryptedStorage() { + encryptedPreferences.edit().apply { + encryptedPreferences.all.keys.forEach { remove(it) } + }.apply() + } - fun saveToEncryptedStorage(account: Account) { - 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)) } - }.apply() - } + fun saveToEncryptedStorage(account: Account) { + 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)) } + }.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 localRelays = gson.fromJson( - getString(PrefKeys.RELAYS, "[]"), - object : TypeToken>() {}.type - ) ?: setOf() + 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 localRelays = gson.fromJson( + getString(PrefKeys.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(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() + val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language - val zapAmountChoices = gson.fromJson( - getString(PrefKeys.ZAP_AMOUNTS, "[]"), - object : TypeToken>() {}.type - ) ?: listOf(500L, 1000L, 5000L) + val zapAmountChoices = gson.fromJson( + getString(PrefKeys.ZAP_AMOUNTS, "[]"), + object : TypeToken>() {}.type + ) ?: listOf(500L, 1000L, 5000L) - val latestContactList = try { - getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { - Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent + val latestContactList = try { + getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { + Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent + } + } catch (e: Throwable) { + e.printStackTrace() + null + } + + val languagePreferences = try { + getString(PrefKeys.LANGUAGE_PREFS, null)?.let { + gson.fromJson(it, object : TypeToken>() {}.type) as Map + } ?: mapOf() + } catch (e: Throwable) { + e.printStackTrace() + mapOf() + } + + if (pubKey != null) { + return Account( + Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), + followingChannels, + hiddenUsers, + localRelays, + dontTranslateFrom, + languagePreferences, + translateTo, + zapAmountChoices, + latestContactList + ) + } else { + return null + } } - } catch (e: Throwable) { - e.printStackTrace() - null - } - - val languagePreferences = try { - getString(PrefKeys.LANGUAGE_PREFS, null)?.let { - gson.fromJson(it, object : TypeToken>() {}.type) as Map - } ?: mapOf() - } catch (e: Throwable) { - e.printStackTrace() - mapOf() - } - - if (pubKey != null) { - return Account( - Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), - followingChannels, - hiddenUsers, - localRelays, - dontTranslateFrom, - languagePreferences, - translateTo, - zapAmountChoices, - latestContactList - ) - } else { - return null - } } - } - fun saveLastRead(route: String, timestampInSecs: Long) { - encryptedPreferences.edit().apply { - putLong(PrefKeys.LAST_READ(route), timestampInSecs) - }.apply() - } - - fun loadLastRead(route: String): Long { - encryptedPreferences.run { - return getLong(PrefKeys.LAST_READ(route), 0) + fun saveLastRead(route: String, timestampInSecs: Long) { + encryptedPreferences.edit().apply { + putLong(PrefKeys.LAST_READ(route), timestampInSecs) + }.apply() + } + + fun loadLastRead(route: String): Long { + encryptedPreferences.run { + return getLong(PrefKeys.LAST_READ(route), 0) + } } - } } \ No newline at end of file From 6067a7d890ca7bb8deb3453cae4a9420b2831d1b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 14:33:09 +0800 Subject: [PATCH 5/8] Use new channel() method --- .../vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt | 5 +++-- .../amethyst/ui/screen/loggedIn/ChatroomListScreen.kt | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 6a54db451..46391ad37 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -104,9 +104,10 @@ private fun FeedLoaded( for (note in state.feed.value) { note.event?.let { var route = "" + val channel = note.channel() - if (note.channel != null) { - route = "Channel/${note.channel!!.idHex}" + if (channel != null) { + route = "Channel/${channel.idHex}" } else { val replyAuthorBase = note.mentions?.first() var userToComposeOn = note.author!! diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 72c4a0dd3..253abe625 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -28,8 +29,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.pager.ExperimentalPagerApi From bd8e9da966ee2fec76b15594dfdb7b39a6ecf9ce Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 15:03:11 +0800 Subject: [PATCH 6/8] Swap LaunchedEffect key for clarity --- .../vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 46391ad37..f056afb5b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -99,8 +99,8 @@ private fun FeedLoaded( val notificationCache = notificationCacheState.value ?: return val context = LocalContext.current.applicationContext - if (markAsRead.value) { - LaunchedEffect(key1 = notificationCache) { + LaunchedEffect(key1 = markAsRead.value) { + if (markAsRead.value) { for (note in state.feed.value) { note.event?.let { var route = "" From 3dd5d5c4b2530a3d5a6d0a4befba353a2d0ec00a Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 15:03:46 +0800 Subject: [PATCH 7/8] Add LaunchedEffect key to fix pre-existing bug --- .../com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 926636feb..4a1727088 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -82,7 +82,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr channel?.let { channel -> var hasNewMessages by remember { mutableStateOf(false) } - LaunchedEffect(key1 = notificationCache) { + LaunchedEffect(key1 = notificationCache, key2 = note) { note.createdAt()?.let { hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context) } @@ -125,7 +125,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr userToComposeOn.let { user -> var hasNewMessages by remember { mutableStateOf(false) } - LaunchedEffect(key1 = notificationCache) { + LaunchedEffect(key1 = notificationCache, key2 = note) { noteEvent?.let { hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) } From 8961675a3ec39398ecbf1b0a9739506d0e0f75bb Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 15:18:07 +0800 Subject: [PATCH 8/8] Fix string resource use from earlier PR (already merged) --- .../com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 5964847c8..b67fac0ca 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 @@ -254,7 +254,7 @@ fun ListContent( Spacer(modifier = Modifier.weight(1f)) IconRow( - "Logout", + stringResource(R.string.log_out), R.drawable.ic_logout, MaterialTheme.colors.onBackground, onClick = { accountViewModel.logOff() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec7928ae9..70f2f3d6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,7 +48,7 @@ " Followers" Profile Security Filters - Log out + Logout Show More Lightning Invoice Pay