Mute keywords
This commit is contained in:
@@ -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)
|
||||
|
||||
+236
-8
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user