From 681e3eb44efe7ea6591815f3c29b851989e815e5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 4 Mar 2023 19:11:49 -0500 Subject: [PATCH] Support for NIP-50: Search --- README.md | 2 +- .../vitorpamplona/amethyst/model/Account.kt | 18 ++- .../service/NostrAccountDataSource.kt | 2 +- .../service/NostrChannelDataSource.kt | 2 +- .../service/NostrChatroomDataSource.kt | 2 +- .../service/NostrChatroomListDataSource.kt | 2 +- .../amethyst/service/NostrGlobalDataSource.kt | 2 +- .../amethyst/service/NostrHomeDataSource.kt | 2 +- .../NostrSearchEventOrUserDataSource.kt | 62 +++++--- .../service/NostrSingleChannelDataSource.kt | 2 +- .../service/NostrSingleEventDataSource.kt | 2 +- .../service/NostrSingleUserDataSource.kt | 2 +- .../amethyst/service/NostrThreadDataSource.kt | 2 +- .../service/NostrUserProfileDataSource.kt | 2 +- .../amethyst/service/model/RepostEvent.kt | 1 - .../amethyst/service/relays/Constants.kt | 6 + .../amethyst/service/relays/JsonFilter.kt | 134 ++++++++++++++++++ .../amethyst/service/relays/Relay.kt | 2 +- .../amethyst/service/relays/TypedFilter.kt | 2 +- .../amethyst/ui/actions/NewRelayListView.kt | 19 +++ .../ui/actions/NewRelayListViewModel.kt | 20 ++- .../ui/screen/loggedIn/SearchScreen.kt | 7 +- app/src/main/res/values/strings.xml | 1 + 23 files changed, 258 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt diff --git a/README.md b/README.md index ae056b95a..233343f0d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Amethyst brings the best social network to your Android phone. Just insert your - [x] Identity Verification (NIP-05) - [x] Long-form Content (NIP-23) - [x] Parameterized Replaceable Events (NIP-33) +- [x] Online Relay Search (NIP-50) - [ ] Local Database - [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post - [ ] 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) - [ ] Proof of Work in the Phone (NIP-13, NIP-20) - [ ] Events with a Subject (NIP-14) -- [ ] Online Relay Search (NIP-50) - [ ] Workspaces - [ ] Expiration Support (NIP-40) - [ ] Internationalization diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 906397708..a199789ae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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? { - return userProfile().relays?.map { + var usersRelayList = userProfile().relays?.map { val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() 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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index daa2fe6b6..c1231921b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType 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.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt index 7bbc5a6c4..72c2b5a4a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter object NostrChannelDataSource: NostrDataSource("ChatroomFeed") { var channel: Channel? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 7ad19edad..4bb33fef9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.FeedType 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 object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index cdf92ad24..850e90872 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.relays.FeedType 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 object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index d3ded698e..6b09e522e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType 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 object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 2ff7a001a..c4ba61022 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrHomeDataSource: NostrDataSource("HomeFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 6f3abc42f..03cd8eb20 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -1,17 +1,40 @@ package com.vitorpamplona.amethyst.service 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.TypedFilter -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter import nostr.postr.bechToBytes import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent import nostr.postr.toHex object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { - private var hexToWatch: String? = null + private var searchString: String? = null private fun createAnythingWithIDFilter(): List? { + 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) { return null } @@ -30,6 +53,22 @@ object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { kinds = listOf(MetadataEvent.kind), 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() } - fun search(eventId: String) { - try { - 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() - } catch (e: Exception) { - // Usually when people add an incomplete npub or note. - } + fun search(searchString: String) { + this.searchString = searchString + invalidateFilters() } fun clear() { - hexToWatch = null + searchString = null } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt index 25a7e3b01..4b4df7907 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") { private var channelsToWatch = setOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 90947cca0..be233d97c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -13,7 +13,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import java.util.Date -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 823d1d159..328344a68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.FeedType 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 object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt index 8777c7eb3..52383ac53 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { private var eventToWatch: String? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 04ce33011..8cfe59d40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType 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.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index 74756c6d0..5a9513c21 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -5,7 +5,6 @@ import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.relays.Client import java.util.Date import nostr.postr.Utils -import nostr.postr.toHex class RepostEvent ( id: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index 593076ac1..05ca9e387 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo object Constants { val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS) val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL) + val activeTypesSearch = setOf(FeedType.SEARCH) fun convertDefaultRelays(): Array { return defaultRelays.map { @@ -44,5 +45,10 @@ object Constants { 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.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) } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt new file mode 100644 index 000000000..205ff76f0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt @@ -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? = null, + val authors: List? = null, + val kinds: List? = null, + val tags: Map>? = 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() + 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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 2de931f3a..0804d07d7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -11,7 +11,7 @@ import okhttp3.WebSocket import okhttp3.WebSocketListener enum class FeedType { - FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL + FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH } class Relay( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt index 39816b50e..dcce6b393 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.JsonObject -import nostr.postr.JsonFilter +import com.vitorpamplona.amethyst.service.relays.JsonFilter class TypedFilter( val types: Set, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index 79626ab90..acc2ae430 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.DeleteSweep import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Groups 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.Upload import androidx.compose.runtime.Composable @@ -117,6 +118,7 @@ fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String = onTogglePrivateDMs = { postViewModel.toggleMessages(it) }, onTogglePublicChats = { postViewModel.togglePublicChats(it) }, onToggleGlobal = { postViewModel.toggleGlobal(it) }, + onToggleSearch = { postViewModel.toggleSearch(it) }, onDelete = { postViewModel.deleteRelay(it) } ) @@ -213,6 +215,7 @@ fun ServerConfig( onTogglePrivateDMs: (RelaySetupInfo) -> Unit, onTogglePublicChats: (RelaySetupInfo) -> Unit, onToggleGlobal: (RelaySetupInfo) -> Unit, + onToggleSearch: (RelaySetupInfo) -> Unit, onDelete: (RelaySetupInfo) -> Unit) { 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 + ) + ) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt index af020ca73..5d0c86866 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt @@ -4,7 +4,10 @@ import android.content.Context import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.Account 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.Relay import com.vitorpamplona.amethyst.service.relays.RelayPool import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -31,7 +34,15 @@ class NewRelayListViewModel: ViewModel() { fun clear(ctx: Context) { _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) relayFile.map { @@ -112,6 +123,13 @@ class NewRelayListViewModel: ViewModel() { 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 Iterable.updated(old: T, new: T): List = map { if (it == old) new else it } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 65e5eb519..d77d5ccc3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -150,12 +151,12 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont .filter { it.isNotBlank() } .distinctUntilChanged() .debounce(300) - .collect { + .collectLatest { if (it.removePrefix("npub").removePrefix("note").length >= 4) - onlineSearch.search(it) + onlineSearch.search(it.trim()) searchResults.value = LocalCache.findUsersStartingWith(it) - searchResultsNotes.value = LocalCache.findNotesStartingWith(it) + searchResultsNotes.value = LocalCache.findNotesStartingWith(it).sortedBy { it.createdAt() } searchResultsChannels.value = LocalCache.findChannelsStartingWith(it) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fc0c1fec..7f3e8b53d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,6 +79,7 @@ Private Message Feed Public Chat Feed Global Feed + Search Feed Add a Relay Display Name My display name