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:

![Demo — archived channel disappears from #
autocomplete](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6156/demo-archive-autocomplete.gif)

## 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:
Logan Johnson
2026-08-19 12:57:25 -04:00
committed by GitHub
parent a567dfc2df
commit fe7c6808e7
2 changed files with 155 additions and 14 deletions

View 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"],
);
});

View File

@@ -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;