fix(channels): strip leading hash prefixes from names (#2250)

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
This commit is contained in:
Logan Johnson
2026-07-23 09:56:10 -07:00
committed by GitHub
parent 06e3d82b04
commit d0ab3fdb05
10 changed files with 190 additions and 8 deletions

View File

@@ -148,6 +148,10 @@ pub fn build_create_channel(
about: Option<&str>,
ttl_seconds: Option<i32>,
) -> Result<EventBuilder, String> {
let name = buzz_sdk_pkg::canonical_channel_name(name);
if name.trim().is_empty() {
return Err("channel name is required".into());
}
let mut tags = vec![
tag(vec!["h", &channel_id.to_string()])?,
tag(vec!["name", name])?,
@@ -194,6 +198,10 @@ pub fn build_update_channel(
return Err("visibility must be \"open\" or \"private\"".into());
}
}
let name = name.map(buzz_sdk_pkg::canonical_channel_name);
if name.is_some_and(|name| name.trim().is_empty()) {
return Err("channel name is required".into());
}
let mut tags = vec![tag(vec!["h", &channel_id.to_string()])?];
if let Some(n) = name {
tags.push(tag(vec!["name", n])?);
@@ -842,7 +850,12 @@ pub fn build_approval_deny(token: &str, note: Option<&str>) -> Result<EventBuild
mod tests {
use super::*;
use nostr::Keys;
#[test]
fn channel_builders_reject_hash_only_names() {
let channel_id = Uuid::new_v4();
assert!(build_create_channel(channel_id, "###", "open", "stream", None, None).is_err());
assert!(build_update_channel(channel_id, Some("###"), None, None, None).is_err());
}
/// Builder layout regression for the NIP-IA owner-of-agent archive flow.
/// Compares against `docs/nips/NIP-IA.md` §Vector 1.
#[test]

View File

@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
canonicalChannelName,
channelNamesMatch,
} from "./canonicalChannelName.ts";
test("canonicalChannelName strips interleaved leading hashes and whitespace", () => {
assert.equal(canonicalChannelName("channel"), "channel");
assert.equal(canonicalChannelName("#channel"), "channel");
assert.equal(canonicalChannelName(" ### channel "), "channel");
assert.equal(canonicalChannelName("# #"), "");
assert.equal(canonicalChannelName("### ###"), "");
assert.equal(canonicalChannelName("channel#topic"), "channel#topic");
});
test("channelNamesMatch canonicalizes both legacy names and search input", () => {
assert.equal(channelNamesMatch("#general", "general"), true);
assert.equal(channelNamesMatch("general", " #GENERAL "), true);
assert.equal(channelNamesMatch("#random", "general"), false);
});

View File

@@ -0,0 +1,14 @@
/**
* Returns the stored form of a channel name after removing the display prefix.
* Keep this aligned with `buzz_core::channel::canonical_channel_name`.
*/
export function canonicalChannelName(name: string): string {
return name.replace(/^[#\s]+/u, "").trimEnd();
}
export function channelNamesMatch(left: string, right: string): boolean {
return (
canonicalChannelName(left).toLowerCase() ===
canonicalChannelName(right).toLowerCase()
);
}

View File

@@ -9,6 +9,10 @@ import {
} from "lucide-react";
import type { Channel } from "@/shared/api/types";
import {
canonicalChannelName,
channelNamesMatch,
} from "@/features/channels/lib/canonicalChannelName";
import { scoreChannelMatch } from "@/features/channels/lib/channelSearchScore";
import {
type ChannelSortMode,
@@ -127,8 +131,9 @@ export function ChannelBrowserDialog({
left: 0,
width: 0,
});
const deferredQuery = React.useDeferredValue(query.trim().toLowerCase());
const trimmedQuery = query.trim();
const canonicalQuery = canonicalChannelName(query);
const deferredQuery = React.useDeferredValue(canonicalQuery.toLowerCase());
const trimmedQuery = canonicalQuery;
// Immediate (non-deferred) lowercased query. The create row's visibility
// (via hasExactMatch) and its label both read from the live query so they
// can never disagree for a frame while the fuzzy filter catches up.
@@ -244,7 +249,7 @@ export function ChannelBrowserDialog({
channels.some(
(channel) =>
channel.channelType !== "dm" &&
channel.name.toLowerCase() === normalizedQuery &&
channelNamesMatch(channel.name, normalizedQuery) &&
(channelTypeFilter
? channel.channelType === channelTypeFilter
: true),