Mute keywords

This commit is contained in:
Vitor Pamplona
2023-09-24 15:10:50 -04:00
parent 57dfe3af8c
commit f4da2ae6be
16 changed files with 843 additions and 74 deletions
@@ -105,6 +105,7 @@ class Account(
data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>,
val spammers: ImmutableSet<String>,
val hiddenWords: ImmutableSet<String>,
val showSensitiveContent: Boolean?
)
@@ -129,23 +130,25 @@ class Account(
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(),
hiddenWords = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
} else {
blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "")
val liveBlockedWords = blockList.publicAndPrivateWords(blockList.decryptedContent ?: "")
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers,
spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(),
hiddenWords = liveBlockedWords,
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
}
} else {
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
hiddenWords = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
@@ -153,8 +156,10 @@ class Account(
}
} else {
val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey)
val liveBlockedWords = blockList?.publicAndPrivateWords(keyPair.privKey)
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
hiddenWords = liveBlockedWords ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
@@ -2276,6 +2281,157 @@ class Account(
return returningList
}
fun hideWord(word: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (loginWithExternalSigner) {
val id = blockList?.id
val encryptedContent = if (id == null) {
val privateTags = listOf(listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted")
val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
ExternalSignerUtils.content.remove("encrypted")
if (encryptedContent.isBlank()) return
encryptedContent
} else {
var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
if (decryptedContent == null) {
ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), id)
val content = ExternalSignerUtils.content[id] ?: ""
if (content.isBlank()) return
decryptedContent = content
}
val privateTags = blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), id)
val eventContent = ExternalSignerUtils.content[id] ?: ""
if (eventContent.isBlank()) return
eventContent
}
var event = if (blockList != null) {
PeopleListEvent.addWord(
earlierVersion = blockList,
word = word,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
)
} else {
PeopleListEvent.createListWithWord(
name = PeopleListEvent.blockList,
word = word,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
)
}
ExternalSignerUtils.openSigner(event)
val eventContent = ExternalSignerUtils.content[event.id] ?: ""
if (eventContent.isBlank()) return
event = PeopleListEvent(
event.id,
event.pubKey,
event.createdAt,
event.tags,
event.content,
eventContent
)
Client.send(event)
LocalCache.consume(event)
} else {
val event = if (blockList != null) {
PeopleListEvent.addWord(
earlierVersion = blockList,
word = word,
isPrivate = true,
privateKey = keyPair.privKey!!
)
} else {
PeopleListEvent.createListWithWord(
name = PeopleListEvent.blockList,
word = word,
isPrivate = true,
privateKey = keyPair.privKey!!
)
}
Client.send(event)
LocalCache.consume(event)
}
live.invalidateData()
saveable.invalidateData()
}
fun showWord(word: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (blockList != null) {
if (loginWithExternalSigner) {
val content = blockList.content
val encryptedContent = if (content.isBlank()) {
val privateTags = listOf(listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id)
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
if (eventContent.isBlank()) return
eventContent
} else {
var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
if (decryptedContent == null) {
ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), blockList.id)
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
if (eventContent.isBlank()) return
decryptedContent = eventContent
}
val privateTags = blockList.privateTagsOrEmpty(decryptedContent).minus(element = listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id)
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
if (eventContent.isBlank()) return
eventContent
}
var event = PeopleListEvent.removeTag(
earlierVersion = blockList,
tag = word,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
)
ExternalSignerUtils.openSigner(event)
val eventContent = ExternalSignerUtils.content[event.id] ?: ""
if (eventContent.isBlank()) return
event = PeopleListEvent.create(event, eventContent)
Client.send(event)
LocalCache.consume(event)
} else {
val event = PeopleListEvent.removeWord(
earlierVersion = blockList,
word = word,
isPrivate = true,
privateKey = keyPair.privKey!!
)
Client.send(event)
LocalCache.consume(event)
}
}
transientHiddenUsers = (transientHiddenUsers - word).toImmutableSet()
live.invalidateData()
saveable.invalidateData()
}
fun hideUser(pubkeyHex: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (loginWithExternalSigner) {
@@ -2284,7 +2440,7 @@ class Account(
val privateTags = listOf(listOf("p", pubkeyHex))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt")
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted")
val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
ExternalSignerUtils.content.remove("encrypted")
if (encryptedContent.isBlank()) return
@@ -2393,9 +2549,9 @@ class Account(
eventContent
}
var event = PeopleListEvent.addUser(
var event = PeopleListEvent.removeTag(
earlierVersion = blockList,
pubKeyHex = pubkeyHex,
tag = pubkeyHex,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
@@ -673,16 +673,24 @@ open class Note(val idHex: String) {
}
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
if (event == null) return false
val thisEvent = event ?: return false
val isBoostedNoteHidden = if (event is GenericRepostEvent || event is RepostEvent || event is CommunityPostApprovalEvent) {
val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) {
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else {
false
}
val isSensitive = event?.isSensitive() ?: false
return isBoostedNoteHidden ||
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
accountChoices.hiddenWords.any {
thisEvent.content.contains(it, true)
}
} else {
false
}
val isSensitive = thisEvent.isSensitive()
return isBoostedNoteHidden || isHiddenByWord ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false)
@@ -32,6 +32,32 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
}
}
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
override fun showHiddenKey(): Boolean {
return true
}
override fun feed(): List<String> {
val blockList = account.getBlockList()
val decryptedContent = blockList?.decryptedContent ?: ""
if (account.loginWithExternalSigner) {
if (decryptedContent.isEmpty()) return emptyList()
return blockList
?.publicAndPrivateWords(decryptedContent)?.toList()
?: emptyList()
}
return blockList
?.publicAndPrivateWords(account.keyPair.privKey)?.toList()
?: emptyList()
}
}
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
@@ -292,7 +292,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
SendButton() {
accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true)
}
@@ -338,7 +338,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
SendButton() {
accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true)
}
@@ -356,7 +356,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
}
@Composable
fun UserStatusSendButton(onClick: () -> Unit) {
fun SendButton(onClick: () -> Unit) {
IconButton(
modifier = Size26Modifier,
onClick = onClick
@@ -258,9 +258,7 @@ fun FeedError(errorMessage: String, onRefresh: () -> Unit) {
@Composable
fun FeedEmpty(onRefresh: () -> Unit) {
Column(
Modifier
.fillMaxHeight()
.fillMaxWidth(),
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -0,0 +1,11 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.MutableState
import kotlinx.collections.immutable.ImmutableList
sealed class StringFeedState {
object Loading : StringFeedState()
class Loaded(val feed: MutableState<ImmutableList<String>>) : StringFeedState()
object Empty : StringFeedState()
class FeedError(val errorMessage: String) : StringFeedState()
}
@@ -0,0 +1,121 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@Composable
fun RefreshingFeedStringFeedView(
viewModel: StringFeedViewModel,
enablePullRefresh: Boolean = true,
inner: @Composable (String) -> Unit
) {
RefresheableView(viewModel, enablePullRefresh) {
StringFeedView(viewModel, inner = inner)
}
}
@Composable
fun StringFeedView(
viewModel: StringFeedViewModel,
pre: (@Composable () -> Unit)? = null,
post: (@Composable () -> Unit)? = null,
inner: @Composable (String) -> Unit
) {
val feedState by viewModel.feedContent.collectAsState()
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is StringFeedState.Empty -> {
StringFeedEmpty(pre, post) {
viewModel.invalidateData()
}
}
is StringFeedState.FeedError -> {
FeedError(state.errorMessage) {
viewModel.invalidateData()
}
}
is StringFeedState.Loaded -> {
FeedLoaded(state, pre, post, inner)
}
is StringFeedState.Loading -> {
LoadingFeed()
}
}
}
}
@Composable
fun StringFeedEmpty(
pre: (@Composable () -> Unit)? = null,
post: (@Composable () -> Unit)? = null,
onRefresh: () -> Unit
) {
Column() {
pre?.let { it() }
Column(
Modifier.weight(1f).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(stringResource(R.string.feed_is_empty))
OutlinedButton(onClick = onRefresh) {
Text(text = stringResource(R.string.refresh))
}
}
post?.let { it() }
}
}
@Composable
private fun FeedLoaded(
state: StringFeedState.Loaded,
pre: (@Composable () -> Unit)? = null,
post: (@Composable () -> Unit)? = null,
inner: @Composable (String) -> Unit
) {
val listState = rememberLazyListState()
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
item {
pre?.let { it() }
}
itemsIndexed(state.feed.value, key = { _, item -> item }) { _, item ->
inner(item)
}
item {
post?.let { it() }
}
}
}
@@ -0,0 +1,107 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.HiddenWordsFeedFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class NostrHiddenWordsFeedViewModel(val account: Account) : StringFeedViewModel(
HiddenWordsFeedFilter(account)
) {
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <NostrHiddenWordsFeedViewModel : ViewModel> create(modelClass: Class<NostrHiddenWordsFeedViewModel>): NostrHiddenWordsFeedViewModel {
return NostrHiddenWordsFeedViewModel(account) as NostrHiddenWordsFeedViewModel
}
}
}
@Stable
open class StringFeedViewModel(val dataSource: FeedFilter<String>) : ViewModel(), InvalidatableViewModel {
private val _feedContent = MutableStateFlow<StringFeedState>(StringFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
private fun refresh() {
viewModelScope.launch(Dispatchers.Default) {
refreshSuspended()
}
}
private fun refreshSuspended() {
checkNotInMainThread()
val notes = dataSource.loadTop().toImmutableList()
val oldNotesState = _feedContent.value
if (oldNotesState is StringFeedState.Loaded) {
// Using size as a proxy for has changed.
if (!equalImmutableLists(notes, oldNotesState.feed.value)) {
updateFeed(notes)
}
} else {
updateFeed(notes)
}
}
private fun updateFeed(notes: ImmutableList<String>) {
viewModelScope.launch(Dispatchers.Main) {
val currentState = _feedContent.value
if (notes.isEmpty()) {
_feedContent.update { StringFeedState.Empty }
} else if (currentState is StringFeedState.Loaded) {
// updates the current list
currentState.feed.value = notes
} else {
_feedContent.update { StringFeedState.Loaded(mutableStateOf(notes)) }
}
}
}
private val bundler = BundledUpdate(250, Dispatchers.IO)
override fun invalidateData(ignoreIfDoing: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
bundler.invalidate(ignoreIfDoing) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
refreshSuspended()
}
}
}
var collectorJob: Job? = null
init {
Log.d("Init", this.javaClass.simpleName)
collectorJob = viewModelScope.launch(Dispatchers.IO) {
checkNotInMainThread()
LocalCache.live.newEventBundles.collect { newNotes ->
checkNotInMainThread()
invalidateData()
}
}
}
override fun onCleared() {
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
}
}
@@ -491,6 +491,12 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
}
}
fun hide(word: String) {
viewModelScope.launch(Dispatchers.IO) {
account.hideWord(word)
}
}
fun showUser(pubkeyHex: String) {
viewModelScope.launch(Dispatchers.IO) {
account.showUser(pubkeyHex)
@@ -1,20 +1,31 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Checkbox
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -23,19 +34,35 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.SendButton
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.StringFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
@@ -44,17 +71,22 @@ fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit)
factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account)
)
val hiddenWordsFeedViewModel: NostrHiddenWordsFeedViewModel = viewModel(
factory = NostrHiddenWordsFeedViewModel.Factory(accountViewModel.account)
)
val spammerFeedViewModel: NostrSpammerAccountsFeedViewModel = viewModel(
factory = NostrSpammerAccountsFeedViewModel.Factory(accountViewModel.account)
)
HiddenUsersScreen(hiddenFeedViewModel, spammerFeedViewModel, accountViewModel, nav)
HiddenUsersScreen(hiddenFeedViewModel, hiddenWordsFeedViewModel, spammerFeedViewModel, accountViewModel, nav)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HiddenUsersScreen(
hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel,
hiddenWordsViewModel: NostrHiddenWordsFeedViewModel,
spammerFeedViewModel: NostrSpammerAccountsFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -65,6 +97,7 @@ fun HiddenUsersScreen(
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Hidden Users Start")
hiddenWordsViewModel.invalidateData()
hiddenFeedViewModel.invalidateData()
spammerFeedViewModel.invalidateData()
}
@@ -78,7 +111,7 @@ fun HiddenUsersScreen(
Column(Modifier.fillMaxHeight()) {
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState() { 2 }
val pagerState = rememberPagerState() { 3 }
val coroutineScope = rememberCoroutineScope()
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) }
var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) }
@@ -109,8 +142,9 @@ fun HiddenUsersScreen(
Text(stringResource(R.string.filter_spam_from_strangers))
}
TabRow(
ScrollableTabRow(
backgroundColor = MaterialTheme.colors.background,
edgePadding = 8.dp,
selectedTabIndex = pagerState.currentPage,
modifier = TabRowHeight
) {
@@ -128,25 +162,98 @@ fun HiddenUsersScreen(
Text(text = stringResource(R.string.spamming_users))
}
)
Tab(
selected = pagerState.currentPage == 2,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
text = {
Text(text = stringResource(R.string.hidden_words))
}
)
}
HorizontalPager(state = pagerState) { page ->
when (page) {
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav)
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel) {
RefreshingFeedUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
}
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel) {
RefreshingFeedUserFeedView(spammerFeedViewModel, accountViewModel, nav)
}
2 -> HiddenWordsFeed(hiddenWordsViewModel, accountViewModel)
}
}
}
}
}
@Composable
private fun HiddenWordsFeed(
hiddenWordsViewModel: NostrHiddenWordsFeedViewModel,
accountViewModel: AccountViewModel
) {
RefresheableView(hiddenWordsViewModel, false) {
StringFeedView(
hiddenWordsViewModel,
post = { AddMuteWordTextField(accountViewModel) }
) {
MutedWordHeader(tag = it, account = accountViewModel)
}
}
}
@Composable
private fun AddMuteWordTextField(accountViewModel: AccountViewModel) {
Row {
val currentWordToAdd = remember {
mutableStateOf("")
}
val hasChanged by remember {
derivedStateOf {
currentWordToAdd.value != ""
}
}
OutlinedTextField(
value = currentWordToAdd.value,
onValueChange = { currentWordToAdd.value = it },
label = { Text(text = stringResource(R.string.hide_new_word_label)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.hide_new_word_label),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Send,
capitalization = KeyboardCapitalization.Sentences
),
keyboardActions = KeyboardActions(
onSend = {
accountViewModel.hide(currentWordToAdd.value)
currentWordToAdd.value = ""
}
),
singleLine = true,
trailingIcon = {
if (hasChanged) {
SendButton {
accountViewModel.hide(currentWordToAdd.value)
currentWordToAdd.value = ""
}
}
}
)
}
}
@Composable
fun RefreshingUserFeedView(
feedViewModel: UserFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
inner: @Composable () -> Unit
) {
WatchAccountAndBlockList(feedViewModel, accountViewModel)
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav)
inner()
}
@Composable
@@ -161,3 +268,124 @@ fun WatchAccountAndBlockList(
feedViewModel.invalidateData()
}
}
@Composable
fun MutedWordHeader(tag: String, modifier: Modifier = StdPadding, account: AccountViewModel) {
Column(
Modifier.fillMaxWidth()
) {
Column(modifier = modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
tag,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
MutedWordActionOptions(tag, account)
}
}
Divider(
thickness = 0.25.dp
)
}
}
@Composable
fun MutedWordActionOptions(
word: String,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val isMutedWord by accountViewModel.account.liveHiddenUsers.map {
word in it.hiddenWords
}.distinctUntilChanged().observeAsState()
if (isMutedWord == true) {
ShowWordButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.showWord(word)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.showWord(word)
}
}
}
} else {
HideWordButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.hideWord(word)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.hideWord(word)
}
}
}
}
}
@Composable
fun HideWordButton(onClick: () -> Unit) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.block_only), color = Color.White)
}
}
@Composable
fun ShowWordButton(text: Int = R.string.unblock, onClick: () -> Unit) {
Button(
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
}
}
+3
View File
@@ -595,4 +595,7 @@
<string name="no_wallet_found_with_error">No Wallets found to pay a lightning invoice (Error: %1$s). Please install a Lightning wallet to use zaps</string>
<string name="no_wallet_found">No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps</string>
<string name="hidden_words">Hidden Words</string>
<string name="hide_new_word_label">Hide new word or sentence</string>
</resources>