Skip to content

fix(chat): create community channels on demand from channel search - #1989

Merged
feruzm merged 7 commits into
developfrom
fix/chat-search-create-community-channel
Oct 24, 2026
Merged

feruzm merged 7 commits into
developfrom
fix/chat-search-create-community-channel

Conversation

@feruzm

@feruzm feruzm commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Since #1987, chat bootstrap only auto-joins a user's first 10 subscribed communities, so other community channels may not exist yet. Channel search only returned existing channels, and on mobile search is the only way to reach a community chat.

  • Channel search also looks the query up on Hive and returns matching communities: a hive-NNNNNN id directly, or (for 3+ characters) communities whose title contains the query, top 3.
  • A match without a channel gets one (ensureChannelForCommunity, real Hive communities only). Creating spends a per-user budget of 10 per hour (Redis, fails closed); finding an existing channel is free, and a budgeted create only follows a clear not-found from Mattermost.
  • The Hive lookup is skipped for private-only searches, bounded by a 5s timeout, and never fails the search. Unexpected failures are logged once per request.
  • Web channel search waits 350ms for typing to pause (mobile already does) and hides the previous term's results meanwhile. Non-empty Hive matches are cached per process for 60s.

No mobile change needed: mobile already lists search results and joins by channel id (it cannot join a not-yet-created channel, which is why creation happens at search time rather than on join).

Known limits, intentionally out of scope:

  • Opening a community's chat page (bootstrap with community) still creates that channel without the budget; that user is joining it.
  • The budget is per user with no global ceiling; total creation is bounded by the number of Hive communities.

Test plan

  • mattermost-channel-search-communities.spec.ts: creation and append, title narrowing, id lookup, cap, skip conditions, budget spend/refusal, single caller lookup, failures, caching, non-array passthrough.
  • chat-channel-create-budget.spec.ts: cap per user, fail closed, expiry always set even when Redis fails midway.
  • mattermost-ensure-user.spec.ts: allowCreate only asked after Hive validation and a clear not-found.
  • channel-search-debounce.spec.tsx: one request per typing pause; stale results hidden until the new term settles.
  • Each guard mutation-checked; pnpm test src/specs/api/ src/specs/features/chat/: 1182 passed.

Closes #1988

Bootstrap now auto-joins only a user's first subscribed communities, so
other community channels may not exist yet and could not be found from
search (the only way to reach them on mobile). Search now also looks the
query up on Hive (community id, or title for 3+ characters, top 3) and
ensures those communities have a channel, appending any the Mattermost
search did not return. Lookup failures never fail the search.

Closes #1988
Channel search can now ask Hive and create channels, so the web hook waits
350ms for typing to pause (same as mobile), and the route keeps short
per-process caches of Hive matches (60s) and community channel ids (5 min).
Hive helpers turn errors into an empty result, so caching empties could hide
communities for a minute after a blip. Channel id cache drops to 60s.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T19:16:23.375538Z 1a67309 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 34ac71b3-ea20-4fd4-aed8-9e56a3e0731c
Commits

Reviewing files that changed from the base of the PR and between 2ced2a3 and c644dd7.

Files selected for processing: 9
  • apps/web/src/app/api/mattermost/channels/search/route.ts
  • apps/web/src/features/chat/mattermost-api.ts
  • apps/web/src/server/chat-channel-create-budget.ts
  • apps/web/src/server/hive-community.ts
  • apps/web/src/server/mattermost.ts
  • apps/web/src/specs/api/chat-channel-create-budget.spec.ts
  • apps/web/src/specs/api/mattermost-channel-search-communities.spec.ts
  • apps/web/src/specs/api/mattermost-ensure-user.spec.ts
  • apps/web/src/specs/features/chat/channel-search-debounce.spec.tsx
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Create missing community channels from chat search

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Include matching Hive communities in channel search so unprovisioned chats remain reachable.
• Create missing channels on demand without letting Hive or creation failures break search.
• Debounce web searches and cache successful lookups to limit repeated requests.
Diagram

graph TD
  W["Web search hook"] --> R["Search route"] --> M["Mattermost search"] --> O["Merged results"]
  R --> H["Hive lookup"] --> C["Ensure channel"] --> O
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Provision every subscribed community during bootstrap
  • ➕ Channels would exist before users search for them.
  • ➖ Restores potentially expensive eager provisioning and does not help discover unsubscribed communities.

Recommendation: Keep on-demand provisioning in the shared search route. It addresses mobile discovery without a mobile change, while the match cap, timeout signal, debounce, and short caches limit the additional work.

Files changed (5) +330 / -8

Enhancement (2) +28 / -1
mattermost-api.tsDebounce web channel search requests +8/-1

Debounce web channel search requests

• The channel search hook waits 350 milliseconds after typing stops before changing its query, reducing requests that can trigger Hive lookups and channel creation.

apps/web/src/features/chat/mattermost-api.ts

hive-community.tsAdd a failure-tolerant Hive community search helper +20/-0

Add a failure-tolerant Hive community search helper

• A new helper queries Hive for ranked community matches, retains entries with string names, and returns an empty list when the lookup fails.

apps/web/src/server/hive-community.ts

Bug fix (1) +98 / -7
route.tsAppend channels for missing Hive community matches +98/-7

Append channels for missing Hive community matches

• Public channel searches now look up a community ID directly or search Hive for up to three matches, then ensure missing channels exist and append them after Mattermost results. The route skips private-only searches, preserves anomalous non-array responses, isolates fallback failures, and uses short per-process caches for successful lookups and channel IDs.

apps/web/src/app/api/mattermost/channels/search/route.ts

Tests (2) +204 / -0
mattermost-channel-search-communities.spec.tsCover community fallback in the channel search route +154/-0

Cover community fallback in the channel search route

• Route tests cover creation and ordering, duplicate avoidance, direct ID lookup, match limits, skipped lookups, creation failure, cache reuse and empty-result retry, and non-array passthrough.

apps/web/src/specs/api/mattermost-channel-search-communities.spec.ts

channel-search-debounce.spec.tsxVerify rapid typing produces one channel search +50/-0

Verify rapid typing produces one channel search

• A hook test uses fake timers to confirm that successive terms produce no request until the debounce expires, then search only the final term.

apps/web/src/specs/features/chat/channel-search-debounce.spec.tsx

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Three new helpers omit return types ✓ Resolved
Description
readCache, writeCache, and ensureCommunityChannelId rely on inferred return types instead of
declaring them explicitly. Later changes to their branches or generic values can alter the public
internal contracts without a type annotation documenting the intended result.
Code

apps/web/src/app/api/mattermost/channels/search/route.ts[R39-49]

+function readCache<T>(cache: Map<string, { at: number; value: T }>, key: string, ttl: number) {
+  const hit = cache.get(key);
+  if (hit && Date.now() - hit.at < ttl) return hit.value;
+  cache.delete(key);
+  return undefined;
+}
+
+function writeCache<T>(cache: Map<string, { at: number; value: T }>, key: string, value: T) {
+  if (cache.size >= MAX_CACHE_ENTRIES) cache.clear();
+  cache.set(key, { at: Date.now(), value });
+}
Evidence
The new helper declarations in the changed route add typed parameters but omit return type
annotations, while the checklist requires explicit annotations for function parameters and return
types in newly added or modified TypeScript code.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/app/api/mattermost/channels/search/route.ts[39-49]
apps/web/src/app/api/mattermost/channels/search/route.ts[70-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new helper functions `readCache`, `writeCache`, and `ensureCommunityChannelId` omit explicit return type annotations, contrary to the TypeScript typing requirement.
## Fix Focus Areas
- apps/web/src/app/api/mattermost/channels/search/route.ts[39-49]
- apps/web/src/app/api/mattermost/channels/search/route.ts[70-76]
## Recommended Fix
Add explicit return types to each helper, including the `undefined` branch for `readCache`, `void` for `writeCache`, and `Promise<string>` for `ensureCommunityChannelId`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Old channels appear under a new search ✓ Resolved
Description
useMattermostChannelSearch keeps the previous query and its results for 350 ms after term
changes. When someone edits a completed search, the chat modal immediately shows the new input but
can still offer public or private channels from the old search without a loading indicator.
Code

apps/web/src/features/chat/mattermost-api.ts[R455-456]

+  const [query, setQuery] = useState(() => term.trim());
+  useDebounce(() => setQuery(term.trim()), CHANNEL_SEARCH_DEBOUNCE_MS, [term]);
Evidence
The hook delays updating query but keys the search results by that delayed value. The consumer
passes the current searchTerm to the hook and renders its returned channels whenever the current
term is searchable, without checking which term produced those channels.

apps/web/src/features/chat/mattermost-api.ts[452-465]
apps/web/src/app/chats/_components/chats-client.tsx[58-87]
apps/web/src/app/chats/_components/chats-client.tsx[190-198]
apps/web/src/app/chats/_components/chats-client.tsx[801-826]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The channel-search hook exposes results for its previous debounced query while the chat modal displays the newly typed term.
## Fix Focus Areas
- apps/web/src/features/chat/mattermost-api.ts[454-475]
- apps/web/src/app/chats/_components/chats-client.tsx[190-198]
## Recommended Fix
Expose the debounced query alongside the results, or suppress results whenever the trimmed current term differs from that query. Keep the request debounce, but render joinable results only for the term that produced them.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Search can offer a channel that is gone ✓ Resolved
Description
ensureCommunityChannelId returns a cached Mattermost ID for up to 60 seconds without checking
whether that channel still exists. If a channel is deleted or recreated during that interval, a
subsequent search offers the old ID, which the join endpoint uses directly.
Code

apps/web/src/app/api/mattermost/channels/search/route.ts[R72-75]

+  const cached = readCache(channelIdCache, key, CHANNEL_ID_TTL_MS);
+  if (cached) return cached;
+  const id = await ensureChannelForCommunity(community.name, community.title, signal);
+  writeCache(channelIdCache, key, id);
Evidence
The new cache returns its ID without calling the authoritative name lookup in
ensureChannelForCommunity. Search places that ID in its response, and the join route posts the
supplied ID directly to Mattermost.

apps/web/src/app/api/mattermost/channels/search/route.ts[70-76]
apps/web/src/app/api/mattermost/channels/search/route.ts[94-102]
apps/web/src/server/mattermost.ts[321-350]
apps/web/src/app/api/mattermost/channels/[channelId]/join/route.ts[20-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A cached community channel ID can outlive the Mattermost channel it identifies, causing search to return an ID that cannot be joined.
## Fix Focus Areas
- apps/web/src/app/api/mattermost/channels/search/route.ts[70-76]
## Recommended Fix
Validate a cached ID against Mattermost before including it in search results, and invalidate and re-ensure the community channel if it no longer exists. Preserve the short cache for IDs that remain valid.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
4. Community search failures go unlogged ✓ Resolved
Description
findHiveCommunityChannels discards rejected channel-creation results from Promise.allSettled
without recording their reasons. When the admin-backed lookup or creation fails, search returns its
other results, but operators cannot identify the failed community or error from this path.
Code

apps/web/src/app/api/mattermost/channels/search/route.ts[102]

+  return results.flatMap((r) => (r.status === "fulfilled" ? [r.value] : []));
Evidence
The new search path keeps only fulfilled settled results. Unlike the existing bootstrap path, which
records failed community-channel attempts, it never inspects or logs rejected results.

apps/web/src/app/api/mattermost/channels/search/route.ts[90-103]
apps/web/src/server/mattermost.ts[334-377]
apps/web/src/app/api/mattermost/bootstrap/route.ts[248-275]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Channel creation failures are intentionally omitted from search results, but their error details are also discarded.
## Fix Focus Areas
- apps/web/src/app/api/mattermost/channels/search/route.ts[90-103]
## Recommended Fix
Associate each settled result with its community and log rejected creations with the community name and original error before returning fulfilled results. Continue returning the Mattermost search results when a creation fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can choose which labels appear on a finding, and whether they show icons or text

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with a non-blocking edge case that can leave a user's search creation budget stuck.

Fix All in Claude CodeFindings

  1. P2 Search budget can stay stuck ▶

Summary

Community channel search now finds matching Hive communities and creates missing channels within a per-user budget.

  • Channel search finds Hive communities and creates missing chat channels.
  • Web channel search waits for typing to pause before it searches.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[User pauses typing] --> B[Search Mattermost channels]
  B --> C{Public search?}
  C -- No --> H[Return search results]
  C -- Yes --> D[Find matching Hive communities]
  D --> E{Channel exists?}
  E -- Yes --> H
  E -- No --> F[Validate community and check user budget]
  F -- Allowed --> G[Create public channel]
  F -- Refused --> H
  G --> H
Loading

Reviews (2) · Last reviewed commit: "docs(chat): note Redis 7 requirement for..." · Reviewed by Greptile

Search now only uses communities that clearly match (exact id, or a title
containing the query) and creating a missing channel spends a per-user
hourly budget (10, Redis, fails closed). Existing channels are always
returned. Drops the channel id cache, logs community channels that could
not be provided, and hides the previous term's results while a new search
is pending on web.
Set the counter with its expiry before counting, so a Redis failure between
calls cannot leave a key that never expires. A budgeted create only follows
a clear not-found, so a Mattermost outage cannot spend the budget. The
search hook returns only the fields its consumers use, and expected
refusals are not logged.
@feruzm
feruzm merged commit 79e38a8 into develop Oct 24, 2026
9 checks passed
@feruzm
feruzm deleted the fix/chat-search-create-community-channel branch October 24, 2026 19:41
Comment on lines +28 to +33
await redis.set(key, 0, "PX", CHANNEL_CREATE_WINDOW_MS, "NX");
const count = await redis.incr(key);
// The key can expire between the two calls, and INCR then recreates it
// without a TTL. Re-arm it; a no-op whenever SET already set one.
// PEXPIRE ... NX needs Redis 7+ (the stacks run redis:7-alpine).
if (count === 1) await redis.pexpire(key, CHANNEL_CREATE_WINDOW_MS, "NX");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Search budget can stay stuck

If the old key expires between set and incr, incr recreates it without an expiry. If pexpire then fails, the catch returns false but leaves the key behind. Later calls cannot repair it: set with NX does nothing, and the count is greater than one.

After ten attempts, the user can no longer create channels from search, even after waiting an hour. This is a non-blocking edge case; make the increment and expiry atomic so the budget always resets.

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat search should create community channels on demand

1 participant