Skip to content

feat(channels): add thread @-mentions and an Activity page#3158

Draft
k11kirky wants to merge 3 commits into
posthog-code/channel-share-linksfrom
posthog-code/channel-thread-mentions
Draft

feat(channels): add thread @-mentions and an Activity page#3158
k11kirky wants to merge 3 commits into
posthog-code/channel-share-linksfrom
posthog-code/channel-thread-mentions

Conversation

@k11kirky

@k11kirky k11kirky commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked on #3157 (channel/thread deep links) — review that first; this PR targets its branch and reuses its thread deep-link routing.

Problem

Channel threads (project-bluebird) are multiplayer, but there's no way to pull a teammate into one — you can't @-tag anyone, and nothing tells you a thread needs your attention.

Why

Requested for Bluebird channels: tag any member of your org in a thread and have them see the notification in a new Activity page in the top-left nav, built on the deep links from #3157.

Changes

  • @-mentions in thread composers — typing @ in a thread's "Reply in thread…" box opens a typeahead over the org's members (new /api/organizations/@current/members/ client call). Selecting one inserts an inline @[Name](email) token, so mentions ride in existing message content with no backend change; older clients degrade to readable text. Mentions render as highlighted chips, with a stronger highlight when it's you.
  • Activity page — new /website/activity route plus an Activity item (with unread red-dot badge) in the channels sidebar nav. It lists thread messages across all channels that mention you, newest first; clicking one opens the thread through the channel deep-link route from feat(channels): add shareable deep links for channels and threads #3157, and each row offers the shareable thread link via the same copy-link helper.
  • The feed is derived client-side from the channel task feeds and threads the app already polls (query keys shared with the feed/thread panel), capped to the most recent tasks. A backend mentions index is the natural follow-up if channels outgrow this.
  • Unread state is a persisted last-seen timestamp; opening the page clears the badge.
  • Instrumented via Channel action (mention_member, view_activity, open_mention, nav_click). Everything lives inside the Channels space, which is gated by the project-bluebird flag at the route.

How did you test this?

  • New unit tests: mention token parse/format/match (shared), activity derivation + unseen counting (core), composer caret/query/insert helpers (ui) — 42 tests, all passing.
  • Ran the ui canvas (76), deep-links/router (9), and core canvas (58) suites — all pass.
  • Full-repo pnpm typecheck (22/22) and Biome clean.
  • Not run: end-to-end in the packaged app (no desktop environment in this session).

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

Thread composers in the Channels space get an @-mention typeahead over the
org's members; mentions are stored as inline @[Name](email) tokens in the
message content, so no message schema change is needed. A new Activity page
(nav item with unread badge in the channels sidebar) lists mentions of the
viewer across channel threads, derived client-side from the task feeds and
threads already polled, and opens the thread via the channel deep-link route.

Generated-By: PostHog Code
Task-Id: a2ff8500-aaa6-4ce0-b01a-656d3b2c3b61
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

React Doctor found 1 issue in 1 file · 1 warning.

1 warning

src/features/canvas/components/MentionComposer.tsx

Reviewed by React Doctor for commit f1cb2f1.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(channels): add thread @-mentions an..." | Re-trigger Greptile

Comment on lines +60 to +69
export function applyMention(
text: string,
active: ActiveMentionQuery,
caret: number,
member: UserBasic,
): { text: string; caret: number } {
const token = `${formatMention(userDisplayName(member), member.email)} `;
const before = text.slice(0, active.start);
const next = before + token + text.slice(caret);
return { text: next, caret: before.length + token.length };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Leftover query characters when caret moves backward mid-query

applyMention replaces from active.start to caret, where caret = el.selectionStart and active.query = text.slice(active.start + 1, caret) — these are always equal by construction. The invariant breaks when the user presses ← to move the cursor backward while the popup is still open: syncActive fires and shortens active.query, but the original typed characters after the new caret remain in the text.

Concrete failure: text is "hey @raq", user presses ← (caret moves from 8 to 7), popup still shows Raquel because "ra" still matches, user presses Tab. applyMention("hey @raq", { start:4, query:"ra" }, 7, raquel) slices text.slice(7) = "q" into the tail → message becomes "hey @[Raquel Smith](raquel@posthog.com) q".

A clean fix is to track the furthest caret position seen for the current @-trigger (a tokenEnd field on ActiveMentionQuery, updated in syncActive to Math.max(caret, prev.tokenEnd) whenever prev.start === newActive.start), and use active.tokenEnd in insert instead of el.selectionStart as the replacement end.

Comment on lines +66 to +68
const token = `${formatMention(userDisplayName(member), member.email)} `;
const before = text.slice(0, active.start);
const next = before + token + text.slice(caret);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Double-space when inserting a mention mid-text: token always carries a trailing space, and when text.slice(caret) already starts with a space (i.e. the caret sits immediately before existing text like " can you look"), the result has two consecutive spaces. The existing test at line 1542 asserts this double-space output but the effect is visible in messages. Stripping the trailing space when the next character is already a space avoids it.

Suggested change
const token = `${formatMention(userDisplayName(member), member.email)} `;
const before = text.slice(0, active.start);
const next = before + token + text.slice(caret);
const tail = text.slice(caret);
const base = formatMention(userDisplayName(member), member.email);
const token = tail.startsWith(" ") ? `${base}` : `${base} `;
const before = text.slice(0, active.start);
const next = before + token + tail;

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

k11kirky added 2 commits July 6, 2026 00:18
…mposer

applyMention now replaces the whole @word (caret moved backward mid-query no
longer leaks typed characters into the message) and reuses an existing
following space instead of doubling it. MentionComposer drops its state-sync
effects for ref-guarded render-time adjustments and scrolls in the key
handler, clearing react-doctor's no-adjust-state-on-prop-change errors.
MentionText keys segments by character offset instead of array index.

Generated-By: PostHog Code
Task-Id: a2ff8500-aaa6-4ce0-b01a-656d3b2c3b61
Generated-By: PostHog Code
Task-Id: a2ff8500-aaa6-4ce0-b01a-656d3b2c3b61
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.

1 participant