Merge pull request #177 from maxmoney21m/feature/172-mark-all-read

Fix #172, add "Mark all as read" feature for DMs and chat rooms
This commit is contained in:
Vitor Pamplona
2023-03-04 17:12:53 -05:00
committed by GitHub
7 changed files with 273 additions and 138 deletions

View File

@@ -14,104 +14,109 @@ import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
import nostr.postr.toHex import nostr.postr.toHex
class LocalPreferences(context: Context) { class LocalPreferences(context: Context) {
val encryptedPreferences = EncryptedStorage().preferences(context) private object PrefKeys {
val gson = GsonBuilder().create() 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}" }
}
fun clearEncryptedStorage() { private val encryptedPreferences = EncryptedStorage().preferences(context)
encryptedPreferences.edit().apply { private val gson = GsonBuilder().create()
remove("nostr_privkey")
remove("nostr_pubkey")
remove("following_channels")
remove("hidden_users")
remove("relays")
remove("dontTranslateFrom")
remove("languagePreferences")
remove("translateTo")
remove("zapAmounts")
remove("latestContactList")
}.apply()
}
fun saveToEncryptedStorage(account: Account) { fun clearEncryptedStorage() {
encryptedPreferences.edit().apply { encryptedPreferences.edit().apply {
account.loggedIn.privKey?.let { putString("nostr_privkey", it.toHex()) } encryptedPreferences.all.keys.forEach { remove(it) }
account.loggedIn.pubKey.let { putString("nostr_pubkey", it.toHex()) } }.apply()
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)) }
}.apply()
}
fun loadFromEncryptedStorage(): Account? { fun saveToEncryptedStorage(account: Account) {
encryptedPreferences.apply { encryptedPreferences.edit().apply {
val privKey = getString("nostr_privkey", null) account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) }
val pubKey = getString("nostr_pubkey", null) account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) }
val followingChannels = getStringSet("following_channels", null) ?: setOf() account.followingChannels.let { putStringSet(PrefKeys.FOLLOWING_CHANNELS, it) }
val hiddenUsers = getStringSet("hidden_users", emptySet()) ?: setOf() account.hiddenUsers.let { putStringSet(PrefKeys.HIDDEN_USERS, it) }
val localRelays = gson.fromJson( account.localRelays.let { putString(PrefKeys.RELAYS, gson.toJson(it)) }
getString("relays", "[]"), account.dontTranslateFrom.let { putStringSet(PrefKeys.DONT_TRANSLATE_FROM, it) }
object : TypeToken<Set<RelaySetupInfo>>() {}.type account.languagePreferences.let { putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(it)) }
) ?: setOf<RelaySetupInfo>() 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()
}
val dontTranslateFrom = getStringSet("dontTranslateFrom", null) ?: setOf() fun loadFromEncryptedStorage(): Account? {
val translateTo = getString("translateTo", null) ?: Locale.getDefault().language 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<Set<RelaySetupInfo>>() {}.type
) ?: setOf<RelaySetupInfo>()
val zapAmountChoices = gson.fromJson( val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
getString("zapAmounts", "[]"), val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
object : TypeToken<List<Long>>() {}.type
) ?: listOf(500L, 1000L, 5000L)
val latestContactList = try { val zapAmountChoices = gson.fromJson(
getString("latestContactList", null)?.let { getString(PrefKeys.ZAP_AMOUNTS, "[]"),
Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent object : TypeToken<List<Long>>() {}.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
}
} catch (e: Throwable) {
e.printStackTrace()
null
}
val languagePreferences = try {
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type) as Map<String, String>
} ?: mapOf<String,String>()
} catch (e: Throwable) {
e.printStackTrace()
mapOf<String,String>()
}
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("languagePreferences", null)?.let {
gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type) as Map<String, String>
} ?: mapOf<String,String>()
} catch (e: Throwable) {
e.printStackTrace()
mapOf<String,String>()
}
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) { fun saveLastRead(route: String, timestampInSecs: Long) {
encryptedPreferences.edit().apply { encryptedPreferences.edit().apply {
putLong("last_read_route_${route}", timestampInSecs) putLong(PrefKeys.LAST_READ(route), timestampInSecs)
}.apply() }.apply()
} }
fun loadLastRead(route: String): Long { fun loadLastRead(route: String): Long {
encryptedPreferences.run { encryptedPreferences.run {
return getLong("last_read_route_${route}", 0) return getLong(PrefKeys.LAST_READ(route), 0)
}
} }
}
} }

View File

@@ -254,7 +254,7 @@ fun ListContent(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconRow( IconRow(
"Logout", stringResource(R.string.log_out),
R.drawable.ic_logout, R.drawable.ic_logout,
MaterialTheme.colors.onBackground, MaterialTheme.colors.onBackground,
onClick = { accountViewModel.logOff() } onClick = { accountViewModel.logOff() }

View File

@@ -82,7 +82,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
channel?.let { channel -> channel?.let { channel ->
var hasNewMessages by remember { mutableStateOf<Boolean>(false) } var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache) { LaunchedEffect(key1 = notificationCache, key2 = note) {
note.createdAt()?.let { note.createdAt()?.let {
hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context) hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context)
} }
@@ -125,7 +125,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
userToComposeOn.let { user -> userToComposeOn.let { user ->
var hasNewMessages by remember { mutableStateOf<Boolean>(false) } var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache) { LaunchedEffect(key1 = notificationCache, key2 = note) {
noteEvent?.let { noteEvent?.let {
hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context)
} }

View File

@@ -9,21 +9,30 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { fun ChatroomListFeedView(
viewModel: FeedViewModel,
accountViewModel: AccountViewModel,
navController: NavController,
markAsRead: MutableState<Boolean>
) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle() val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
@@ -45,21 +54,27 @@ fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountView
}, },
) { ) {
Column() { Column() {
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> Crossfade(
targetState = feedState,
animationSpec = tween(durationMillis = 100)
) { state ->
when (state) { when (state) {
is FeedState.Empty -> { is FeedState.Empty -> {
FeedEmpty { FeedEmpty {
isRefreshing = true isRefreshing = true
} }
} }
is FeedState.FeedError -> { is FeedState.FeedError -> {
FeedError(state.errorMessage) { FeedError(state.errorMessage) {
isRefreshing = true isRefreshing = true
} }
} }
is FeedState.Loaded -> { is FeedState.Loaded -> {
FeedLoaded(state, accountViewModel, navController) FeedLoaded(state, accountViewModel, navController, markAsRead)
} }
FeedState.Loading -> { FeedState.Loading -> {
LoadingFeed() LoadingFeed()
} }
@@ -73,10 +88,44 @@ fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountView
private fun FeedLoaded( private fun FeedLoaded(
state: FeedState.Loaded, state: FeedState.Loaded,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController,
markAsRead: MutableState<Boolean>,
) { ) {
val listState = rememberLazyListState() 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
LaunchedEffect(key1 = markAsRead.value) {
if (markAsRead.value) {
for (note in state.feed.value) {
note.event?.let {
var route = ""
val channel = note.channel()
if (channel != null) {
route = "Channel/${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( LazyColumn(
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 10.dp, top = 10.dp,
@@ -84,7 +133,9 @@ private fun FeedLoaded(
), ),
state = listState 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( ChatroomCompose(
item, item,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,

View File

@@ -116,6 +116,4 @@ class AccountViewModel(private val account: Account): ViewModel() {
fun follow(user: User) { fun follow(user: User) {
account.follow(user) account.follow(user)
} }
} }

View File

@@ -1,19 +1,33 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.MaterialTheme
import androidx.compose.material.Tab import androidx.compose.material.Tab
import androidx.compose.material.TabRow import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults import androidx.compose.material.TabRowDefaults
import androidx.compose.material.Text 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.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -26,6 +40,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
@@ -33,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.vitorpamplona.amethyst.R
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
@@ -41,48 +55,78 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
val pagerState = rememberPagerState() val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Column(Modifier.fillMaxHeight()) { var moreActionsExpanded by remember { mutableStateOf(false) }
Column( val markKnownAsRead = remember { mutableStateOf(false) }
modifier = Modifier.padding(vertical = 0.dp) val markNewAsRead = remember { mutableStateOf(false) }
) {
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( Box(Modifier.fillMaxSize()) {
selected = pagerState.currentPage == 1, Column(Modifier.fillMaxHeight()) {
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, Column(
text = { modifier = Modifier.padding(vertical = 0.dp)
Text(text = stringResource(R.string.new_requests)) ) {
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 @Composable
fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { fun TabKnown(
accountViewModel: AccountViewModel,
navController: NavController,
markAsRead: MutableState<Boolean>,
) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
@@ -113,13 +157,17 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) modifier = Modifier.padding(vertical = 0.dp)
) { ) {
ChatroomListFeedView(feedViewModel, accountViewModel, navController) ChatroomListFeedView(feedViewModel, accountViewModel, navController, markAsRead)
} }
} }
} }
@Composable @Composable
fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { fun TabNew(
accountViewModel: AccountViewModel,
navController: NavController,
markAsRead: MutableState<Boolean>
) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
@@ -150,7 +198,37 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) {
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) modifier = Modifier.padding(vertical = 0.dp)
) { ) {
ChatroomListFeedView(feedViewModel, accountViewModel, navController) ChatroomListFeedView(feedViewModel, accountViewModel, navController, markAsRead)
} }
} }
} }
@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))
}
}
}

View File

@@ -48,7 +48,7 @@
<string name="followers">" Followers"</string> <string name="followers">" Followers"</string>
<string name="profile">Profile</string> <string name="profile">Profile</string>
<string name="security_filters">Security Filters</string> <string name="security_filters">Security Filters</string>
<string name="log_out">Log out</string> <string name="log_out">Logout</string>
<string name="show_more">Show More</string> <string name="show_more">Show More</string>
<string name="lightning_invoice">Lightning Invoice</string> <string name="lightning_invoice">Lightning Invoice</string>
<string name="pay">Pay</string> <string name="pay">Pay</string>
@@ -173,6 +173,9 @@
<string name="report_hateful_speech">Report Hateful speech</string> <string name="report_hateful_speech">Report Hateful speech</string>
<string name="report_nudity_porn">Report Nudity / Porn</string> <string name="report_nudity_porn">Report Nudity / Porn</string>
<string name="others">others</string> <string name="others">others</string>
<string name="mark_all_known_as_read">Mark all Known as read</string>
<string name="mark_all_new_as_read">Mark all New as read</string>
<string name="mark_all_as_read">Mark all as read</string>
<string name="account_backup_tips_md"> <string name="account_backup_tips_md">
## Key Backup and Safety Tips ## Key Backup and Safety Tips
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity. \n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.