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( data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>, val hiddenUsers: ImmutableSet<String>,
val spammers: ImmutableSet<String>, val spammers: ImmutableSet<String>,
val hiddenWords: ImmutableSet<String>,
val showSensitiveContent: Boolean? val showSensitiveContent: Boolean?
) )
@@ -129,23 +130,25 @@ class Account(
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = persistentSetOf(), hiddenUsers = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers hiddenWords = persistentSetOf(),
?: persistentSetOf(), spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent showSensitiveContent = showSensitiveContent
) )
} else { } else {
blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id] blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "") val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "")
val liveBlockedWords = blockList.publicAndPrivateWords(blockList.decryptedContent ?: "")
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = liveBlockedUsers, hiddenUsers = liveBlockedUsers,
spammers = localLive?.account?.transientHiddenUsers hiddenWords = liveBlockedWords,
?: persistentSetOf(), spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent showSensitiveContent = showSensitiveContent
) )
} }
} else { } else {
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = persistentSetOf(), hiddenUsers = persistentSetOf(),
hiddenWords = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(), ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent showSensitiveContent = showSensitiveContent
@@ -153,8 +156,10 @@ class Account(
} }
} else { } else {
val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey) val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey)
val liveBlockedWords = blockList?.publicAndPrivateWords(keyPair.privKey)
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(), hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
hiddenWords = liveBlockedWords ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent showSensitiveContent = showSensitiveContent
) )
@@ -2276,6 +2281,157 @@ class Account(
return returningList 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) { fun hideUser(pubkeyHex: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList()) val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (loginWithExternalSigner) { if (loginWithExternalSigner) {
@@ -2284,7 +2440,7 @@ class Account(
val privateTags = listOf(listOf("p", pubkeyHex)) val privateTags = listOf(listOf("p", pubkeyHex))
val msg = Event.mapper.writeValueAsString(privateTags) 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"] ?: "" val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
ExternalSignerUtils.content.remove("encrypted") ExternalSignerUtils.content.remove("encrypted")
if (encryptedContent.isBlank()) return if (encryptedContent.isBlank()) return
@@ -2393,9 +2549,9 @@ class Account(
eventContent eventContent
} }
var event = PeopleListEvent.addUser( var event = PeopleListEvent.removeTag(
earlierVersion = blockList, earlierVersion = blockList,
pubKeyHex = pubkeyHex, tag = pubkeyHex,
isPrivate = true, isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(), pubKey = keyPair.pubKey.toHexKey(),
encryptedContent encryptedContent
@@ -673,16 +673,24 @@ open class Note(val idHex: String) {
} }
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { 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 replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else { } else {
false false
} }
val isSensitive = event?.isSensitive() ?: false val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
return isBoostedNoteHidden || accountChoices.hiddenWords.any {
thisEvent.content.contains(it, true)
}
} else {
false
}
val isSensitive = thisEvent.isSensitive()
return isBoostedNoteHidden || isHiddenByWord ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) || accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) || accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false) (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>() { class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feedKey(): String { override fun feedKey(): String {
return account.userProfile().pubkeyHex return account.userProfile().pubkeyHex
@@ -292,7 +292,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
if (hasChanged) { if (hasChanged) {
UserStatusSendButton() { SendButton() {
accountViewModel.createStatus(currentStatus.value) accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true) focusManager.clearFocus(true)
} }
@@ -338,7 +338,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
if (hasChanged) { if (hasChanged) {
UserStatusSendButton() { SendButton() {
accountViewModel.updateStatus(it, thisStatus.value) accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true) focusManager.clearFocus(true)
} }
@@ -356,7 +356,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
} }
@Composable @Composable
fun UserStatusSendButton(onClick: () -> Unit) { fun SendButton(onClick: () -> Unit) {
IconButton( IconButton(
modifier = Size26Modifier, modifier = Size26Modifier,
onClick = onClick onClick = onClick
@@ -258,9 +258,7 @@ fun FeedError(errorMessage: String, onRefresh: () -> Unit) {
@Composable @Composable
fun FeedEmpty(onRefresh: () -> Unit) { fun FeedEmpty(onRefresh: () -> Unit) {
Column( Column(
Modifier Modifier.fillMaxSize(),
.fillMaxHeight()
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center 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) { fun showUser(pubkeyHex: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
account.showUser(pubkeyHex) account.showUser(pubkeyHex)
@@ -1,20 +1,31 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState 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.Checkbox
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text import androidx.compose.material.Text
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.derivedStateOf
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.mutableStateOf
@@ -23,19 +34,35 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource 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.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R 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.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel 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.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.StringFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel 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.TabRowHeight
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -44,17 +71,22 @@ fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit)
factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account) factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account)
) )
val hiddenWordsFeedViewModel: NostrHiddenWordsFeedViewModel = viewModel(
factory = NostrHiddenWordsFeedViewModel.Factory(accountViewModel.account)
)
val spammerFeedViewModel: NostrSpammerAccountsFeedViewModel = viewModel( val spammerFeedViewModel: NostrSpammerAccountsFeedViewModel = viewModel(
factory = NostrSpammerAccountsFeedViewModel.Factory(accountViewModel.account) factory = NostrSpammerAccountsFeedViewModel.Factory(accountViewModel.account)
) )
HiddenUsersScreen(hiddenFeedViewModel, spammerFeedViewModel, accountViewModel, nav) HiddenUsersScreen(hiddenFeedViewModel, hiddenWordsFeedViewModel, spammerFeedViewModel, accountViewModel, nav)
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun HiddenUsersScreen( fun HiddenUsersScreen(
hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel, hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel,
hiddenWordsViewModel: NostrHiddenWordsFeedViewModel,
spammerFeedViewModel: NostrSpammerAccountsFeedViewModel, spammerFeedViewModel: NostrSpammerAccountsFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -65,6 +97,7 @@ fun HiddenUsersScreen(
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Hidden Users Start") println("Hidden Users Start")
hiddenWordsViewModel.invalidateData()
hiddenFeedViewModel.invalidateData() hiddenFeedViewModel.invalidateData()
spammerFeedViewModel.invalidateData() spammerFeedViewModel.invalidateData()
} }
@@ -78,7 +111,7 @@ fun HiddenUsersScreen(
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState() { 2 } val pagerState = rememberPagerState() { 3 }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) } var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) }
var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) } var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) }
@@ -109,8 +142,9 @@ fun HiddenUsersScreen(
Text(stringResource(R.string.filter_spam_from_strangers)) Text(stringResource(R.string.filter_spam_from_strangers))
} }
TabRow( ScrollableTabRow(
backgroundColor = MaterialTheme.colors.background, backgroundColor = MaterialTheme.colors.background,
edgePadding = 8.dp,
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
modifier = TabRowHeight modifier = TabRowHeight
) { ) {
@@ -128,25 +162,98 @@ fun HiddenUsersScreen(
Text(text = stringResource(R.string.spamming_users)) 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 -> HorizontalPager(state = pagerState) { page ->
when (page) { when (page) {
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav) 0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel) {
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav) 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 @Composable
fun RefreshingUserFeedView( fun RefreshingUserFeedView(
feedViewModel: UserFeedViewModel, feedViewModel: UserFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit inner: @Composable () -> Unit
) { ) {
WatchAccountAndBlockList(feedViewModel, accountViewModel) WatchAccountAndBlockList(feedViewModel, accountViewModel)
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav) inner()
} }
@Composable @Composable
@@ -161,3 +268,124 @@ fun WatchAccountAndBlockList(
feedViewModel.invalidateData() 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_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="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> </resources>
@@ -16,6 +16,7 @@ class BookmarkListEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
var decryptedContent = "" var decryptedContent = ""
companion object { companion object {
@@ -125,14 +125,20 @@ open class Event(
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) } override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex } override fun isTagged(key: String, tag: String) = tags.any { it.size > 1 && it[0] == key && it[1] == tag }
override fun isTaggedUsers(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "p" && it[1] in idHexes }
override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex } override fun isAnyTagged(key: String, tags: Set<String>) = this.tags.any { it.size > 1 && it[0] == key && it[1] in tags }
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex } override fun isTaggedWord(word: String) = isTagged("word", word)
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes } override fun isTaggedUser(idHex: String) = isTagged("p", idHex)
override fun isTaggedUsers(idHexes: Set<String>) = isAnyTagged("p", idHexes)
override fun isTaggedEvent(idHex: String) = isTagged("e", idHex)
override fun isTaggedAddressableNote(idHex: String) = isTagged("a", idHex)
override fun isTaggedAddressableNotes(idHexes: Set<String>) = isAnyTagged( "a", idHexes)
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) } override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
@@ -29,6 +29,10 @@ interface EventInterface {
fun hasValidSignature(): Boolean fun hasValidSignature(): Boolean
fun isTagged(key: String, tag: String): Boolean
fun isAnyTagged(key: String, tags: Set<String>): Boolean
fun isTaggedWord(word: String): Boolean
fun isTaggedUser(idHex: String): Boolean fun isTaggedUser(idHex: String): Boolean
fun isTaggedUsers(idHex: Set<String>): Boolean fun isTaggedUsers(idHex: Set<String>): Boolean
@@ -66,7 +66,8 @@ abstract class GeneralListEvent(
return privateTagsCache return privateTagsCache
} }
fun privateTagsOrEmpty(privKey: ByteArray): List<List<String>> { fun privateTagsOrEmpty(privKey: ByteArray?): List<List<String>> {
if (privKey == null) return emptyList()
return privateTags(privKey) ?: emptyList() return privateTags(privKey) ?: emptyList()
} }
@@ -18,62 +18,94 @@ class PeopleListEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
var decryptedContent: String? = null var decryptedContent: String? = null
@Transient
var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null
@Transient
var publicAndPrivateWordCache: ImmutableSet<String>? = null
fun filterTagList(key: String, privateTags: List<List<String>>?): ImmutableSet<String> {
val privateUserList = privateTags?.let {
it.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet()
} ?: emptySet()
val publicUserList = tags.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet()
return (privateUserList + publicUserList).toImmutableSet()
}
fun publicAndPrivateWords(privateKey: ByteArray?): ImmutableSet<String> {
publicAndPrivateWordCache?.let {
return it
}
publicAndPrivateWordCache = filterTagList("word", privateTagsOrEmpty(privKey = privateKey))
return publicAndPrivateWordCache ?: persistentSetOf()
}
fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> { fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> {
publicAndPrivateUserCache?.let { publicAndPrivateUserCache?.let {
return it return it
} }
val privateUserList = privateKey?.let { publicAndPrivateUserCache = filterTagList("p", privateTagsOrEmpty(privKey = privateKey))
privateTagsOrEmpty(privKey = it).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
} ?: emptySet()
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
return publicAndPrivateUserCache ?: persistentSetOf() return publicAndPrivateUserCache ?: persistentSetOf()
} }
fun publicAndPrivateWords(decryptedContent: String): ImmutableSet<HexKey> {
publicAndPrivateWordCache?.let {
return it
}
publicAndPrivateWordCache = filterTagList("word", privateTagsOrEmpty(decryptedContent))
return publicAndPrivateWordCache ?: persistentSetOf()
}
fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet<HexKey> { fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet<HexKey> {
publicAndPrivateUserCache?.let { publicAndPrivateUserCache?.let {
return it return it
} }
val privateUserList = privateTagsOrEmpty(decryptedContent).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet() publicAndPrivateUserCache = filterTagList("p", privateTagsOrEmpty(decryptedContent))
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
return publicAndPrivateUserCache ?: persistentSetOf() return publicAndPrivateUserCache ?: persistentSetOf()
} }
fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean { fun isTagged(key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray): Boolean {
return if (isPrivate) { return if (isPrivate) {
privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex } privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == key && it[1] == tag }
} else { } else {
isTaggedUser(idHex) isTagged(key, tag)
} }
} }
fun isTaggedUser(idHex: String, isPrivate: Boolean, content: String): Boolean { fun isTagged(key: String, tag: String, isPrivate: Boolean, decryptedContent: String): Boolean {
return if (isPrivate) { return if (isPrivate) {
privateTagsOrEmpty(content).any { it.size > 1 && it[0] == "p" && it[1] == idHex } privateTagsOrEmpty(decryptedContent).any { it.size > 1 && it[0] == key && it[1] == tag }
} else { } else {
isTaggedUser(idHex) isTagged(key, tag)
} }
} }
fun isTaggedWord(word: String, isPrivate: Boolean, privateKey: ByteArray) = isTagged( "word", word, isPrivate, privateKey)
fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray) = isTagged( "p", idHex, isPrivate, privateKey)
fun isTaggedUser(idHex: String, isPrivate: Boolean, decryptedContent: String) = isTagged( "p", idHex, isPrivate, decryptedContent)
fun isTaggedWord(idHex: String, isPrivate: Boolean, decryptedContent: String) = isTagged( "word", idHex, isPrivate, decryptedContent)
companion object { companion object {
const val kind = 30000 const val kind = 30000
const val blockList = "mute" const val blockList = "mute"
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun createListWithTag(name: String, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) { return if (isPrivate) {
create( create(
content = encryptTags(listOf(listOf("p", pubKeyHex)), privateKey), content = encryptTags(listOf(listOf(key, tag)), privateKey),
tags = listOf(listOf("d", name)), tags = listOf(listOf("d", name)),
privateKey = privateKey, privateKey = privateKey,
createdAt = createdAt createdAt = createdAt
@@ -81,13 +113,21 @@ class PeopleListEvent(
} else { } else {
create( create(
content = "", content = "",
tags = listOf(listOf("d", name), listOf("p", pubKeyHex)), tags = listOf(listOf("d", name), listOf(key, tag)),
privateKey = privateKey, privateKey = privateKey,
createdAt = createdAt createdAt = createdAt
) )
} }
} }
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return createListWithTag(name, "p", pubKeyHex, isPrivate, privateKey, createdAt)
}
fun createListWithWord(name: String, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return createListWithTag(name, "word", word, isPrivate, privateKey, createdAt)
}
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) { return if (isPrivate) {
create( create(
@@ -106,6 +146,24 @@ class PeopleListEvent(
} }
} }
fun createListWithWord(name: String, word: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = listOf(listOf("d", name)),
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = "",
tags = listOf(listOf("d", name), listOf("word", word)),
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) { return if (isPrivate) {
create( create(
@@ -135,6 +193,24 @@ class PeopleListEvent(
} }
} }
fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = earlierVersion.tags,
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("word", word)),
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) { return if (isPrivate) {
create( create(
@@ -153,13 +229,40 @@ class PeopleListEvent(
} }
} }
fun removeTag(earlierVersion: PeopleListEvent, tag: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = earlierVersion.tags,
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != tag },
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return addTag(earlierVersion, "word", word, isPrivate, privateKey, createdAt)
}
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion return addTag(earlierVersion, "p", pubKeyHex, isPrivate, privateKey, createdAt)
}
fun addTag(earlierVersion: PeopleListEvent, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (earlierVersion.isTagged(key, tag, isPrivate, privateKey)) return earlierVersion
return if (isPrivate) { return if (isPrivate) {
create( create(
content = encryptTags( content = encryptTags(
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf("p", pubKeyHex)), privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf(key, tag)),
privateKey = privateKey privateKey = privateKey
), ),
tags = earlierVersion.tags, tags = earlierVersion.tags,
@@ -169,48 +272,38 @@ class PeopleListEvent(
} else { } else {
create( create(
content = earlierVersion.content, content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)), tags = earlierVersion.tags.plus(element = listOf(key, tag)),
privateKey = privateKey, privateKey = privateKey,
createdAt = createdAt createdAt = createdAt
) )
} }
} }
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, encryptedContent: String, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun removeWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) { return removeTag(earlierVersion, "word", word, isPrivate, privateKey, createdAt)
create(
content = encryptedContent,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
pubKey = pubKey,
createdAt = createdAt
)
}
} }
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (!earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, privateKey, createdAt)
}
fun removeTag(earlierVersion: PeopleListEvent, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (!earlierVersion.isTagged(key, tag, isPrivate, privateKey)) return earlierVersion
return if (isPrivate) { return if (isPrivate) {
create( create(
content = encryptTags( content = encryptTags(
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && it[1] != pubKeyHex }, privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && !(it[0] == key && it[1] == tag) },
privateKey = privateKey privateKey = privateKey
), ),
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }, tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) },
privateKey = privateKey, privateKey = privateKey,
createdAt = createdAt createdAt = createdAt
) )
} else { } else {
create( create(
content = earlierVersion.content, content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }, tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) },
privateKey = privateKey, privateKey = privateKey,
createdAt = createdAt createdAt = createdAt
) )