Adds hidden words filter to search, hashtag and geotag feeds

Applies hidden words even to hashtags that were not included in the content of the event.
Pre-process search to avoid showing and hiding posts after hidden words where processed by the UI.
This commit is contained in:
Vitor Pamplona 2024-10-24 11:00:07 -04:00
parent f88985b2bf
commit 518e1c88ce
11 changed files with 92 additions and 37 deletions

View File

@ -2043,7 +2043,10 @@ object LocalCache {
}
}
fun findUsersStartingWith(username: String): List<User> {
fun findUsersStartingWith(
username: String,
forAccount: Account?,
): List<User> {
checkNotInMainThread()
val key = decodePublicKeyAsHexOrNull(username)
@ -2056,13 +2059,19 @@ object LocalCache {
}
return users.filter { _, user: User ->
(user.anyNameStartsWith(username)) ||
user.pubkeyHex.startsWith(username, true) ||
user.pubkeyNpub().startsWith(username, true)
(
(user.anyNameStartsWith(username)) ||
user.pubkeyHex.startsWith(username, true) ||
user.pubkeyNpub().startsWith(username, true)
) &&
(forAccount == null || (!forAccount.isHidden(user) && !user.containsAny(forAccount.flowHiddenUsers.value.hiddenWordsCase)))
}
}
fun findNotesStartingWith(text: String): List<Note> {
fun findNotesStartingWith(
text: String,
forAccount: Account,
): List<Note> {
checkNotInMainThread()
val key = decodeEventIdAsHexOrNull(text)
@ -2089,11 +2098,19 @@ object LocalCache {
note.idHex.startsWith(text, true) ||
note.idNote().startsWith(text, true)
) {
return@filter true
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter true
} else {
return@filter false
}
}
if (note.event?.isContentEncoded() == false) {
return@filter note.event?.content()?.contains(text, true) ?: false
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter note.event?.content()?.contains(text, true) ?: false
} else {
return@filter false
}
}
return@filter false
@ -2112,11 +2129,19 @@ object LocalCache {
if (addressable.event?.matchTag1With(text) == true ||
addressable.idHex.startsWith(text, true)
) {
return@filter true
if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter true
} else {
return@filter false
}
}
if (addressable.event?.isContentEncoded() == false) {
return@filter addressable.event?.content()?.contains(text, true) ?: false
if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter addressable.event?.content()?.contains(text, true) ?: false
} else {
return@filter false
}
}
return@filter false

View File

@ -771,29 +771,11 @@ open class Note(
return true
}
if (author?.toBestDisplayName()?.containsAny(accountChoices.hiddenWordsCase) == true) {
if (thisEvent.anyHashTag { it.containsAny(accountChoices.hiddenWordsCase) }) {
return true
}
if (author?.profilePicture()?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.banner?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.about?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.lud06?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.info?.lud16?.containsAny(accountChoices.hiddenWordsCase) == true) {
return true
}
if (author?.containsAny(accountChoices.hiddenWordsCase) == true) return true
}
return false

View File

@ -45,6 +45,8 @@ import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import com.vitorpamplona.quartz.utils.DualCase
import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -370,6 +372,36 @@ class User(
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
} != null
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
if (hiddenWordsCase.isEmpty()) return false
if (toBestDisplayName().containsAny(hiddenWordsCase)) {
return true
}
if (profilePicture()?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.banner?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.about?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.lud06?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.lud16?.containsAny(hiddenWordsCase) == true) {
return true
}
return false
}
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
var liveSet: UserLiveSet? = null

View File

@ -274,7 +274,7 @@ open class EditPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed()
}

View File

@ -1002,7 +1002,7 @@ open class NewPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed()
}
@ -1031,7 +1031,7 @@ open class NewPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed()
}
@ -1059,7 +1059,7 @@ open class NewPostViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
userSuggestions =
LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.findUsersStartingWith(lastWord.removePrefix("@"), account)
.sortedWith(
compareBy(
{ account?.isFollowing(it) },

View File

@ -64,6 +64,7 @@ class GeoHashFeedFilter(
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedGeoHash(geoTag) == true &&
!it.isHiddenFor(account.flowHiddenUsers.value) &&
account.isAcceptable(it)
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)

View File

@ -68,6 +68,7 @@ class HashtagFeedFilter(
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedHash(hashTag) == true &&
!it.isHiddenFor(account.flowHiddenUsers.value) &&
account.isAcceptable(it)
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)

View File

@ -75,7 +75,7 @@ class SearchBarViewModel(
_hashtagResults.emit(findHashtags(searchValue))
_searchResultsUsers.emit(
LocalCache
.findUsersStartingWith(searchValue)
.findUsersStartingWith(searchValue, account)
.sortedWith(
compareBy(
{ it.toBestDisplayName().startsWith(searchValue, true) },
@ -86,7 +86,7 @@ class SearchBarViewModel(
)
_searchResultsNotes.emit(
LocalCache
.findNotesStartingWith(searchValue)
.findNotesStartingWith(searchValue, account)
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed(),
)

View File

@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -178,7 +177,7 @@ fun WatchAccountForSearchScreen(accountViewModel: AccountViewModel) {
}
}
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
@OptIn(FlowPreview::class)
@Composable
private fun SearchBar(
searchBarViewModel: SearchBarViewModel,

View File

@ -107,6 +107,19 @@ open class Event(
}
}
override fun anyHashTag(onEach: (str: String) -> Boolean) = anyTagged("t", onEach)
private fun anyTagged(
tagName: String,
onEach: (str: String) -> Boolean,
) = tags.any {
if (it.size > 1 && it[0] == tagName) {
onEach(it[1])
} else {
false
}
}
override fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R) = mapTagged("e", map)
override fun <R> mapTaggedAddress(map: (address: String) -> R) = mapTagged("a", map)

View File

@ -121,6 +121,8 @@ interface EventInterface {
fun forEachHashTag(onEach: (eventId: HexKey) -> Unit)
fun anyHashTag(onEach: (str: String) -> Boolean): Boolean
fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R): List<R>
fun <R> mapTaggedAddress(map: (address: String) -> R): List<R>