mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-06-10 22:10:49 +02:00
Support for NIP-50: Search
This commit is contained in:
parent
5cc851ecc8
commit
681e3eb44e
@ -29,6 +29,7 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
|||||||
- [x] Identity Verification (NIP-05)
|
- [x] Identity Verification (NIP-05)
|
||||||
- [x] Long-form Content (NIP-23)
|
- [x] Long-form Content (NIP-23)
|
||||||
- [x] Parameterized Replaceable Events (NIP-33)
|
- [x] Parameterized Replaceable Events (NIP-33)
|
||||||
|
- [x] Online Relay Search (NIP-50)
|
||||||
- [ ] Local Database
|
- [ ] Local Database
|
||||||
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
||||||
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
||||||
@ -38,7 +39,6 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
|||||||
- [ ] Generic Tags (NIP-12)
|
- [ ] Generic Tags (NIP-12)
|
||||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||||
- [ ] Events with a Subject (NIP-14)
|
- [ ] Events with a Subject (NIP-14)
|
||||||
- [ ] Online Relay Search (NIP-50)
|
|
||||||
- [ ] Workspaces
|
- [ ] Workspaces
|
||||||
- [ ] Expiration Support (NIP-40)
|
- [ ] Expiration Support (NIP-40)
|
||||||
- [ ] Internationalization
|
- [ ] Internationalization
|
||||||
|
@ -452,11 +452,25 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takes a User's relay list and adds the types of feeds they are active for.
|
||||||
fun activeRelays(): Array<Relay>? {
|
fun activeRelays(): Array<Relay>? {
|
||||||
return userProfile().relays?.map {
|
var usersRelayList = userProfile().relays?.map {
|
||||||
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
|
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
|
||||||
Relay(it.key, it.value.read, it.value.write, localFeedTypes)
|
Relay(it.key, it.value.read, it.value.write, localFeedTypes)
|
||||||
}?.toTypedArray()
|
} ?: return null
|
||||||
|
|
||||||
|
// Ugly, but forces nostr.band as the only search-supporting relay today.
|
||||||
|
// TODO: Remove when search becomes more available.
|
||||||
|
if (usersRelayList.none { it.activeTypes.contains(FeedType.SEARCH) }) {
|
||||||
|
usersRelayList = usersRelayList + Relay(
|
||||||
|
Constants.forcedRelayForSearch.url,
|
||||||
|
Constants.forcedRelayForSearch.read,
|
||||||
|
Constants.forcedRelayForSearch.write,
|
||||||
|
Constants.forcedRelayForSearch.feedTypes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return usersRelayList.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertLocalRelays(): Array<Relay> {
|
fun convertLocalRelays(): Array<Relay> {
|
||||||
|
@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
|
|
||||||
object NostrChannelDataSource: NostrDataSource("ChatroomFeed") {
|
object NostrChannelDataSource: NostrDataSource("ChatroomFeed") {
|
||||||
var channel: Channel? = null
|
var channel: Channel? = null
|
||||||
|
@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
|
||||||
object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") {
|
object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") {
|
||||||
|
@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
|
||||||
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
|
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
|
||||||
|
@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
|
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
|
||||||
|
@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object NostrHomeDataSource: NostrDataSource("HomeFeed") {
|
object NostrHomeDataSource: NostrDataSource("HomeFeed") {
|
||||||
|
@ -1,17 +1,40 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
|
object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
|
||||||
private var hexToWatch: String? = null
|
private var searchString: String? = null
|
||||||
|
|
||||||
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
|
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
|
||||||
|
val mySearchString = searchString
|
||||||
|
if (mySearchString == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val hexToWatch = try {
|
||||||
|
if (mySearchString.startsWith("npub") || mySearchString.startsWith("nsec")) {
|
||||||
|
decodePublicKey(mySearchString).toHex()
|
||||||
|
} else if (mySearchString.startsWith("note")) {
|
||||||
|
mySearchString.bechToBytes().toHex()
|
||||||
|
} else {
|
||||||
|
mySearchString
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Usually when people add an incomplete npub or note.
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
if (hexToWatch == null) {
|
if (hexToWatch == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -30,6 +53,22 @@ object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
kinds = listOf(MetadataEvent.kind),
|
kinds = listOf(MetadataEvent.kind),
|
||||||
authors = listOfNotNull(hexToWatch)
|
authors = listOfNotNull(hexToWatch)
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
TypedFilter(
|
||||||
|
types = FeedType.values().toSet(),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(MetadataEvent.kind),
|
||||||
|
search = mySearchString,
|
||||||
|
limit = 20,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
TypedFilter(
|
||||||
|
types = FeedType.values().toSet(),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ChannelMetadataEvent.kind, ChannelCreateEvent.kind, ChannelMessageEvent.kind),
|
||||||
|
search = mySearchString,
|
||||||
|
limit = 20
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -40,23 +79,12 @@ object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
searchChannel.typedFilters = createAnythingWithIDFilter()
|
searchChannel.typedFilters = createAnythingWithIDFilter()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(eventId: String) {
|
fun search(searchString: String) {
|
||||||
try {
|
this.searchString = searchString
|
||||||
val hex = if (eventId.startsWith("npub") || eventId.startsWith("nsec")) {
|
|
||||||
decodePublicKey(eventId).toHex()
|
|
||||||
} else if (eventId.startsWith("note")) {
|
|
||||||
eventId.bechToBytes().toHex()
|
|
||||||
} else {
|
|
||||||
eventId
|
|
||||||
}
|
|
||||||
hexToWatch = hex
|
|
||||||
invalidateFilters()
|
invalidateFilters()
|
||||||
} catch (e: Exception) {
|
|
||||||
// Usually when people add an incomplete npub or note.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
hexToWatch = null
|
searchString = null
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
|
|
||||||
object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") {
|
object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") {
|
||||||
private var channelsToWatch = setOf<String>()
|
private var channelsToWatch = setOf<String>()
|
||||||
|
@ -13,7 +13,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
||||||
|
@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
|
|
||||||
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
|
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
|
||||||
|
@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
|
|
||||||
object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") {
|
object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") {
|
||||||
private var eventToWatch: String? = null
|
private var eventToWatch: String? = null
|
||||||
|
@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
@ -5,7 +5,6 @@ import com.vitorpamplona.amethyst.model.toHexKey
|
|||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.toHex
|
|
||||||
|
|
||||||
class RepostEvent (
|
class RepostEvent (
|
||||||
id: HexKey,
|
id: HexKey,
|
||||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
|||||||
object Constants {
|
object Constants {
|
||||||
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
|
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
|
||||||
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
||||||
|
val activeTypesSearch = setOf(FeedType.SEARCH)
|
||||||
|
|
||||||
fun convertDefaultRelays(): Array<Relay> {
|
fun convertDefaultRelays(): Array<Relay> {
|
||||||
return defaultRelays.map {
|
return defaultRelays.map {
|
||||||
@ -44,5 +45,10 @@ object Constants {
|
|||||||
RelaySetupInfo("wss://atlas.nostr.land", read = true, write = false, feedTypes = activeTypesGlobalChats),
|
RelaySetupInfo("wss://atlas.nostr.land", read = true, write = false, feedTypes = activeTypesGlobalChats),
|
||||||
RelaySetupInfo("wss://relay.orangepill.dev", read = true, write = false, feedTypes = activeTypesGlobalChats),
|
RelaySetupInfo("wss://relay.orangepill.dev", read = true, write = false, feedTypes = activeTypesGlobalChats),
|
||||||
RelaySetupInfo("wss://relay.nostrati.com", read = true, write = false, feedTypes = activeTypesGlobalChats),
|
RelaySetupInfo("wss://relay.nostrati.com", read = true, write = false, feedTypes = activeTypesGlobalChats),
|
||||||
|
|
||||||
|
// Supporting NIP-50
|
||||||
|
RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val forcedRelayForSearch = RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch)
|
||||||
}
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.relays
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface Filter {
|
||||||
|
fun match(event: Event): Boolean
|
||||||
|
fun toShortString(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonFilter(
|
||||||
|
val ids: List<String>? = null,
|
||||||
|
val authors: List<String>? = null,
|
||||||
|
val kinds: List<Int>? = null,
|
||||||
|
val tags: Map<String, List<String>>? = null,
|
||||||
|
val since: Long? = null,
|
||||||
|
val until: Long? = null,
|
||||||
|
val limit: Int? = null,
|
||||||
|
val search: String? = null,
|
||||||
|
) : Filter, Serializable {
|
||||||
|
fun toJson(): String {
|
||||||
|
val jsonObject = JsonObject()
|
||||||
|
ids?.run {
|
||||||
|
jsonObject.add("ids", JsonArray().apply { ids.forEach { add(it) } })
|
||||||
|
}
|
||||||
|
authors?.run {
|
||||||
|
jsonObject.add("authors", JsonArray().apply { authors.forEach { add(it) } })
|
||||||
|
}
|
||||||
|
kinds?.run {
|
||||||
|
jsonObject.add("kinds", JsonArray().apply { kinds.forEach { add(it) } })
|
||||||
|
}
|
||||||
|
tags?.run {
|
||||||
|
entries.forEach { kv ->
|
||||||
|
jsonObject.add("#${kv.key}", JsonArray().apply { kv.value.forEach { add(it) } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
since?.run {
|
||||||
|
jsonObject.addProperty("since", since)
|
||||||
|
}
|
||||||
|
until?.run {
|
||||||
|
jsonObject.addProperty("until", until)
|
||||||
|
}
|
||||||
|
limit?.run {
|
||||||
|
jsonObject.addProperty("limit", limit)
|
||||||
|
}
|
||||||
|
search?.run {
|
||||||
|
jsonObject.addProperty("search", search)
|
||||||
|
}
|
||||||
|
return gson.toJson(jsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun match(event: Event): Boolean {
|
||||||
|
if (ids?.any { event.id == it } == false) return false
|
||||||
|
if (kinds?.any { event.kind == it } == false) return false
|
||||||
|
if (authors?.any { event.pubKey == it } == false) return false
|
||||||
|
tags?.forEach { tag ->
|
||||||
|
if (!event.tags.any { it.first() == tag.key && it[1] in tag.value }) return false
|
||||||
|
}
|
||||||
|
if (event.createdAt !in (since ?: Long.MIN_VALUE)..(until ?: Long.MAX_VALUE))
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "JsonFilter${toJson()}"
|
||||||
|
|
||||||
|
override fun toShortString(): String {
|
||||||
|
val list = ArrayList<String>()
|
||||||
|
ids?.run {
|
||||||
|
list.add("ids")
|
||||||
|
}
|
||||||
|
authors?.run {
|
||||||
|
list.add("authors")
|
||||||
|
}
|
||||||
|
kinds?.run {
|
||||||
|
list.add("kinds[${kinds.joinToString()}]")
|
||||||
|
}
|
||||||
|
tags?.run {
|
||||||
|
list.add("tags")
|
||||||
|
}
|
||||||
|
since?.run {
|
||||||
|
list.add("since")
|
||||||
|
}
|
||||||
|
until?.run {
|
||||||
|
list.add("until")
|
||||||
|
}
|
||||||
|
limit?.run {
|
||||||
|
list.add("limit")
|
||||||
|
}
|
||||||
|
search?.run {
|
||||||
|
list.add("search")
|
||||||
|
}
|
||||||
|
return list.joinToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val gson: Gson = GsonBuilder().create()
|
||||||
|
|
||||||
|
fun fromJson(json: String): JsonFilter {
|
||||||
|
val jsonFilter = gson.fromJson(json, JsonObject::class.java)
|
||||||
|
return fromJson(jsonFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
val declaredFields = JsonFilter::class.java.declaredFields.map { it.name }
|
||||||
|
fun fromJson(json: JsonObject): JsonFilter {
|
||||||
|
// sanity check
|
||||||
|
if (json.keySet().any { !(it.startsWith("#") || it in declaredFields) }) {
|
||||||
|
println("Filter $json contains unknown parameters.")
|
||||||
|
}
|
||||||
|
return JsonFilter(
|
||||||
|
ids = if (json.has("ids")) json.getAsJsonArray("ids").map { it.asString } else null,
|
||||||
|
authors = if (json.has("authors")) json.getAsJsonArray("authors")
|
||||||
|
.map { it.asString } else null,
|
||||||
|
kinds = if (json.has("kinds")) json.getAsJsonArray("kinds")
|
||||||
|
.map { it.asInt } else null,
|
||||||
|
tags = json
|
||||||
|
.entrySet()
|
||||||
|
.filter { it.key.startsWith("#") }
|
||||||
|
.associate {
|
||||||
|
it.key.substring(1) to it.value.asJsonArray.map { it.asString }
|
||||||
|
}
|
||||||
|
.ifEmpty { null },
|
||||||
|
since = if (json.has("since")) json.get("since").asLong else null,
|
||||||
|
until = if (json.has("until")) json.get("until").asLong else null,
|
||||||
|
limit = if (json.has("limit")) json.get("limit").asInt else null,
|
||||||
|
search = if (json.has("search")) json.get("search").asString else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ import okhttp3.WebSocket
|
|||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
|
|
||||||
enum class FeedType {
|
enum class FeedType {
|
||||||
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL
|
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH
|
||||||
}
|
}
|
||||||
|
|
||||||
class Relay(
|
class Relay(
|
||||||
|
@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import nostr.postr.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
|
|
||||||
class TypedFilter(
|
class TypedFilter(
|
||||||
val types: Set<FeedType>,
|
val types: Set<FeedType>,
|
||||||
|
@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.DeleteSweep
|
|||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.Groups
|
import androidx.compose.material.icons.filled.Groups
|
||||||
import androidx.compose.material.icons.filled.Public
|
import androidx.compose.material.icons.filled.Public
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.SyncProblem
|
import androidx.compose.material.icons.filled.SyncProblem
|
||||||
import androidx.compose.material.icons.filled.Upload
|
import androidx.compose.material.icons.filled.Upload
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -117,6 +118,7 @@ fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String =
|
|||||||
onTogglePrivateDMs = { postViewModel.toggleMessages(it) },
|
onTogglePrivateDMs = { postViewModel.toggleMessages(it) },
|
||||||
onTogglePublicChats = { postViewModel.togglePublicChats(it) },
|
onTogglePublicChats = { postViewModel.togglePublicChats(it) },
|
||||||
onToggleGlobal = { postViewModel.toggleGlobal(it) },
|
onToggleGlobal = { postViewModel.toggleGlobal(it) },
|
||||||
|
onToggleSearch = { postViewModel.toggleSearch(it) },
|
||||||
|
|
||||||
onDelete = { postViewModel.deleteRelay(it) }
|
onDelete = { postViewModel.deleteRelay(it) }
|
||||||
)
|
)
|
||||||
@ -213,6 +215,7 @@ fun ServerConfig(
|
|||||||
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
|
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
|
||||||
onTogglePublicChats: (RelaySetupInfo) -> Unit,
|
onTogglePublicChats: (RelaySetupInfo) -> Unit,
|
||||||
onToggleGlobal: (RelaySetupInfo) -> Unit,
|
onToggleGlobal: (RelaySetupInfo) -> Unit,
|
||||||
|
onToggleSearch: (RelaySetupInfo) -> Unit,
|
||||||
|
|
||||||
onDelete: (RelaySetupInfo) -> Unit) {
|
onDelete: (RelaySetupInfo) -> Unit) {
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
@ -309,6 +312,22 @@ fun ServerConfig(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
onClick = { onToggleSearch(item) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
stringResource(R.string.search_feed),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
.size(15.dp),
|
||||||
|
tint = if (item.feedTypes.contains(FeedType.SEARCH)) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||||
|
alpha = 0.32f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,10 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@ -31,7 +34,15 @@ class NewRelayListViewModel: ViewModel() {
|
|||||||
|
|
||||||
fun clear(ctx: Context) {
|
fun clear(ctx: Context) {
|
||||||
_relays.update {
|
_relays.update {
|
||||||
val relayFile = account.userProfile().relays
|
var relayFile = account.userProfile().relays
|
||||||
|
|
||||||
|
// Ugly, but forces nostr.band as the only search-supporting relay today.
|
||||||
|
// TODO: Remove when search becomes more available.
|
||||||
|
if (relayFile?.none { it.key == Constants.forcedRelayForSearch.url } == true) {
|
||||||
|
relayFile = relayFile + Pair(
|
||||||
|
Constants.forcedRelayForSearch.url, ContactListEvent.ReadWrite(Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.write)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (relayFile != null)
|
if (relayFile != null)
|
||||||
relayFile.map {
|
relayFile.map {
|
||||||
@ -112,6 +123,13 @@ class NewRelayListViewModel: ViewModel() {
|
|||||||
it.updated(relay, relay.copy( feedTypes = newTypes ))
|
it.updated(relay, relay.copy( feedTypes = newTypes ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleSearch(relay: RelaySetupInfo) {
|
||||||
|
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.SEARCH)
|
||||||
|
_relays.update {
|
||||||
|
it.updated(relay, relay.copy( feedTypes = newTypes ))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Iterable<T>.updated(old: T, new: T): List<T> = map { if (it == old) new else it }
|
fun <T> Iterable<T>.updated(old: T, new: T): List<T> = map { if (it == old) new else it }
|
||||||
|
@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
@ -150,12 +151,12 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
|
|||||||
.filter { it.isNotBlank() }
|
.filter { it.isNotBlank() }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.debounce(300)
|
.debounce(300)
|
||||||
.collect {
|
.collectLatest {
|
||||||
if (it.removePrefix("npub").removePrefix("note").length >= 4)
|
if (it.removePrefix("npub").removePrefix("note").length >= 4)
|
||||||
onlineSearch.search(it)
|
onlineSearch.search(it.trim())
|
||||||
|
|
||||||
searchResults.value = LocalCache.findUsersStartingWith(it)
|
searchResults.value = LocalCache.findUsersStartingWith(it)
|
||||||
searchResultsNotes.value = LocalCache.findNotesStartingWith(it)
|
searchResultsNotes.value = LocalCache.findNotesStartingWith(it).sortedBy { it.createdAt() }
|
||||||
searchResultsChannels.value = LocalCache.findChannelsStartingWith(it)
|
searchResultsChannels.value = LocalCache.findChannelsStartingWith(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,7 @@
|
|||||||
<string name="private_message_feed">Private Message Feed</string>
|
<string name="private_message_feed">Private Message Feed</string>
|
||||||
<string name="public_chat_feed">Public Chat Feed</string>
|
<string name="public_chat_feed">Public Chat Feed</string>
|
||||||
<string name="global_feed">Global Feed</string>
|
<string name="global_feed">Global Feed</string>
|
||||||
|
<string name="search_feed">Search Feed</string>
|
||||||
<string name="add_a_relay">Add a Relay</string>
|
<string name="add_a_relay">Add a Relay</string>
|
||||||
<string name="display_name">Display Name</string>
|
<string name="display_name">Display Name</string>
|
||||||
<string name="my_display_name">My display name</string>
|
<string name="my_display_name">My display name</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user