mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop,mobile): user pickers search by substring, not prefix
Flip the user-search filters from search_mode="prefix" to "contains" so every picker surface (member add, @mentions, DM recipients, topbar people) matches any typed fragment of a name. Message typeahead stays on prefix. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -172,7 +172,7 @@ fn build_user_search_filter(query: &str, limit: usize, page: u32) -> serde_json:
|
||||
serde_json::json!({
|
||||
"kinds": [0],
|
||||
"search": query,
|
||||
"search_mode": "prefix",
|
||||
"search_mode": "contains",
|
||||
"limit": limit,
|
||||
"page": page,
|
||||
})
|
||||
@@ -236,12 +236,14 @@ pub async fn search_users(
|
||||
// than a substring hit in `about`. The caller can request later pages via the
|
||||
// cursor so the UI cap is only a page size, not a terminal directory ceiling.
|
||||
//
|
||||
// `search_mode: "prefix"` matters: every caller of this command is a
|
||||
// `search_mode: "contains"` matters: every caller of this command is a
|
||||
// typeahead surface (member picker, @mention popup, DM recipient search,
|
||||
// topbar people results), so a partially typed name must match. Without it
|
||||
// the relay runs whole-word `websearch_to_tsquery` matching and "tyl"
|
||||
// returns zero results for "Tyler". Same bridge-only extension the topbar
|
||||
// message search uses (see `build_search_messages_filter`).
|
||||
// topbar people results), so any typed fragment of a name must match.
|
||||
// Whole-word FTS returns zero results for "tyl" → "Tyler", and prefix
|
||||
// mode returns zero results for "kurs" → "mattkursmark" because the name
|
||||
// is a single lexeme. Contains mode is ILIKE substring matching on the
|
||||
// relay (same bridge-only `search_mode` extension family the topbar
|
||||
// message search uses via `build_search_messages_filter`).
|
||||
let events = query_relay(&state, &[build_user_search_filter(trimmed, max, page)]).await?;
|
||||
|
||||
let mut response = nostr_convert::rank_user_search_results(&events, trimmed, max);
|
||||
@@ -336,15 +338,17 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn user_search_filter_requests_prefix_mode_for_typeahead() {
|
||||
fn user_search_filter_requests_contains_mode_for_typeahead() {
|
||||
// Every caller of `search_users` is a typeahead surface. Whole-word
|
||||
// FTS matching returns zero results for a partially typed name
|
||||
// ("tyl" for "Tyler"), which reads as "user doesn't exist" in the
|
||||
// member picker and @mention popup. Pin the mode so it can't drift.
|
||||
let filter = build_user_search_filter("tyl", 25, 1);
|
||||
// ("tyl" for "Tyler"), and prefix matching returns zero results for
|
||||
// a mid-name fragment ("kurs" for "mattkursmark"), which reads as
|
||||
// "user doesn't exist" in the member picker and @mention popup. Pin
|
||||
// the mode so it can't drift.
|
||||
let filter = build_user_search_filter("kurs", 25, 1);
|
||||
|
||||
assert_eq!(filter["search"], serde_json::json!("tyl"));
|
||||
assert_eq!(filter["search_mode"], serde_json::json!("prefix"));
|
||||
assert_eq!(filter["search"], serde_json::json!("kurs"));
|
||||
assert_eq!(filter["search_mode"], serde_json::json!("contains"));
|
||||
assert_eq!(filter["limit"], serde_json::json!(25));
|
||||
assert_eq!(filter["page"], serde_json::json!(1));
|
||||
}
|
||||
|
||||
@@ -134,16 +134,17 @@ abstract final class NostrFilters {
|
||||
|
||||
/// Global user search over kind:0 profiles (NIP-50 via the HTTP bridge).
|
||||
///
|
||||
/// `search_mode: "prefix"` is a Buzz bridge-only extension: every caller is
|
||||
/// a typeahead surface, so a partially typed name must match ("rac" →
|
||||
/// "raccoon"). Mirrors desktop's `build_user_search_filter`
|
||||
/// (desktop/src-tauri/src/commands/profile.rs). Bridge-only — send through
|
||||
/// `queryRelay`, not a WebSocket REQ.
|
||||
/// `search_mode: "contains"` is a Buzz bridge-only extension: every caller
|
||||
/// is a typeahead surface, so any typed fragment of a name must match
|
||||
/// ("kurs" → "mattkursmark" — prefix matching misses mid-name fragments
|
||||
/// because names are single FTS lexemes). Mirrors desktop's
|
||||
/// `build_user_search_filter` (desktop/src-tauri/src/commands/profile.rs).
|
||||
/// Bridge-only — send through `queryRelay`, not a WebSocket REQ.
|
||||
static NostrFilter searchUsers(String query, {int limit = 50}) => NostrFilter(
|
||||
kinds: [0],
|
||||
search: query,
|
||||
limit: limit,
|
||||
extensions: const {'search_mode': 'prefix'},
|
||||
extensions: const {'search_mode': 'contains'},
|
||||
);
|
||||
|
||||
/// Deletions (kind:5) targeting event IDs.
|
||||
|
||||
Reference in New Issue
Block a user