mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
fix(desktop): hide archived channels from #/Tab autocomplete (#6156)
🤖 ## Summary Desktop's `#`/Tab channel autocomplete offered archived channels as if they were valid destinations. Stale channels competed with active ones in the picker, including a confusing uppercase `STREAM` entry — the badge of an archived stream channel leaking into the suggestion list. After this change, autocomplete suggests only active, non-DM channels. Links to archived channels in old messages still resolve normally. ## Demo Before/after, real archive flow:  ## What changed `useChannelLinks.ts` builds the autocomplete suggestion list from the raw relay channel list and previously excluded only DMs, so archived channels slipped through. The fix filters `archivedAt === null` while generating suggestions only — the shared navigation list stays unfiltered, so resolving historical links to archived channels is unchanged. - `desktop/src/features/messages/lib/useChannelLinks.ts` — suggestion selection centralized in `selectChannelSuggestions`; excludes archived channels and DMs; keeps the existing eight-result limit. - `desktop/src/features/messages/lib/useChannelLinks.test.mjs` — new tests: archived stream/forum excluded, active ones included, DMs excluded, mixed lists. ## Validation At exact head `5a963b8407bd48f40d992e026d0a841d2445647f`: `pnpm check`, `pnpm typecheck`, and the full Desktop unit suite (4,960 passed). Smoke E2E run compared against exact base `f956e6fe`: the only failures are two pre-existing failures that reproduce on base and four documented pass-on-retry flakes — none touches the autocomplete surface. Signed-off-by: loganj <loganj@squareup.com> Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
121
desktop/src/features/messages/lib/useChannelLinks.test.mjs
Normal file
121
desktop/src/features/messages/lib/useChannelLinks.test.mjs
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Unit tests for selectChannelSuggestions — the #/Tab channel-autocomplete
|
||||
* suggestion list.
|
||||
*
|
||||
* Archived channels must stay resolvable in historical links elsewhere in the
|
||||
* app, but must not be offered as new `#channel` suggestions (they were
|
||||
* leaking through, badged STREAM/FORUM, for archived branch channels).
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { selectChannelSuggestions } from "./useChannelLinks.ts";
|
||||
|
||||
function channel(overrides = {}) {
|
||||
return {
|
||||
id: "chan-1",
|
||||
name: "general",
|
||||
channelType: "stream",
|
||||
archivedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("excludes an archived stream channel from suggestions", () => {
|
||||
const channels = [
|
||||
channel({
|
||||
id: "s1",
|
||||
name: "shipped-feature",
|
||||
channelType: "stream",
|
||||
archivedAt: "2026-01-01T00:00:00Z",
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(selectChannelSuggestions(channels, "shipped"), []);
|
||||
});
|
||||
|
||||
test("excludes an archived forum channel from suggestions", () => {
|
||||
const channels = [
|
||||
channel({
|
||||
id: "f1",
|
||||
name: "old-rfc",
|
||||
channelType: "forum",
|
||||
archivedAt: "2026-01-01T00:00:00Z",
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(selectChannelSuggestions(channels, "old"), []);
|
||||
});
|
||||
|
||||
test("includes an active stream channel matching the query", () => {
|
||||
const channels = [
|
||||
channel({
|
||||
id: "s2",
|
||||
name: "engineering",
|
||||
channelType: "stream",
|
||||
archivedAt: null,
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(selectChannelSuggestions(channels, "eng"), [
|
||||
{ id: "s2", name: "engineering", channelType: "stream" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("includes an active forum channel matching the query", () => {
|
||||
const channels = [
|
||||
channel({
|
||||
id: "f2",
|
||||
name: "design-rfcs",
|
||||
channelType: "forum",
|
||||
archivedAt: null,
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(selectChannelSuggestions(channels, "design"), [
|
||||
{ id: "f2", name: "design-rfcs", channelType: "forum" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("excludes DM channels regardless of archive state", () => {
|
||||
const channels = [
|
||||
channel({ id: "d1", name: "alice", channelType: "dm", archivedAt: null }),
|
||||
];
|
||||
|
||||
assert.deepEqual(selectChannelSuggestions(channels, "alice"), []);
|
||||
});
|
||||
|
||||
test("mixed list keeps only active, non-DM matches", () => {
|
||||
const channels = [
|
||||
channel({
|
||||
id: "s3",
|
||||
name: "random",
|
||||
channelType: "stream",
|
||||
archivedAt: null,
|
||||
}),
|
||||
channel({
|
||||
id: "s4",
|
||||
name: "random-old",
|
||||
channelType: "stream",
|
||||
archivedAt: "2026-01-01T00:00:00Z",
|
||||
}),
|
||||
channel({
|
||||
id: "f3",
|
||||
name: "random-forum",
|
||||
channelType: "forum",
|
||||
archivedAt: null,
|
||||
}),
|
||||
channel({
|
||||
id: "d2",
|
||||
name: "random-dm",
|
||||
channelType: "dm",
|
||||
archivedAt: null,
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(
|
||||
selectChannelSuggestions(channels, "random").map((s) => s.id),
|
||||
["s3", "f3"],
|
||||
);
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
|
||||
import { detectPrefixQuery } from "@/shared/lib/detectPrefixQuery";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import type { AutocompleteEdit } from "./useRichTextEditor";
|
||||
|
||||
export type ChannelSuggestion = {
|
||||
@@ -12,6 +13,37 @@ export type ChannelSuggestion = {
|
||||
|
||||
const CHANNEL_QUERY_DEBOUNCE_MS = 120;
|
||||
|
||||
/**
|
||||
* Archived channels must stay resolvable in historical links (rendered from
|
||||
* the unfiltered ChannelNavigationContext list), but are dead ends for new
|
||||
* `#channel` references — exclude them here, at generation time, rather than
|
||||
* from the shared channel list.
|
||||
*/
|
||||
function isChannelSuggestable(
|
||||
channel: Pick<Channel, "channelType" | "archivedAt">,
|
||||
): boolean {
|
||||
return channel.channelType !== "dm" && channel.archivedAt === null;
|
||||
}
|
||||
|
||||
/** Exported for unit testing. */
|
||||
export function selectChannelSuggestions(
|
||||
channels: Channel[],
|
||||
query: string,
|
||||
): ChannelSuggestion[] {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return channels
|
||||
.filter(
|
||||
(ch) =>
|
||||
isChannelSuggestable(ch) && ch.name.toLowerCase().includes(lowerQuery),
|
||||
)
|
||||
.slice(0, 8)
|
||||
.map((ch) => ({
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
channelType: ch.channelType as "stream" | "forum",
|
||||
}));
|
||||
}
|
||||
|
||||
export function useChannelLinks() {
|
||||
const { channels } = useChannelNavigation();
|
||||
|
||||
@@ -27,7 +59,7 @@ export function useChannelLinks() {
|
||||
|
||||
/** Channel names (original casing) for overlay highlighting. */
|
||||
const knownChannelNames = React.useMemo<string[]>(
|
||||
() => channels.filter((ch) => ch.channelType !== "dm").map((ch) => ch.name),
|
||||
() => channels.filter(isChannelSuggestable).map((ch) => ch.name),
|
||||
[channels],
|
||||
);
|
||||
|
||||
@@ -56,19 +88,7 @@ export function useChannelLinks() {
|
||||
if (channelQuery === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowerQuery = channelQuery.toLowerCase();
|
||||
return channels
|
||||
.filter(
|
||||
(ch) =>
|
||||
ch.channelType !== "dm" && ch.name.toLowerCase().includes(lowerQuery),
|
||||
)
|
||||
.slice(0, 8)
|
||||
.map((ch) => ({
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
channelType: ch.channelType as "stream" | "forum",
|
||||
}));
|
||||
return selectChannelSuggestions(channels, channelQuery);
|
||||
}, [channels, channelQuery]);
|
||||
|
||||
const isChannelOpen = channelQuery !== null && channelSuggestions.length > 0;
|
||||
|
||||
Reference in New Issue
Block a user