mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
14
desktop/src/features/channels/lib/canonicalChannelName.ts
Normal file
14
desktop/src/features/channels/lib/canonicalChannelName.ts
Normal 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()
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user