diff --git a/desktop/src/features/channels/ui/ChannelCanvas.tsx b/desktop/src/features/channels/ui/ChannelCanvas.tsx
index d7ea8616b3..e2ebca6390 100644
--- a/desktop/src/features/channels/ui/ChannelCanvas.tsx
+++ b/desktop/src/features/channels/ui/ChannelCanvas.tsx
@@ -5,6 +5,7 @@ import {
useCanvasQuery,
useSetCanvasMutation,
} from "@/features/channels/hooks";
+import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
import { Button } from "@/shared/ui/button";
import { Markdown } from "@/shared/ui/markdown";
import { Textarea } from "@/shared/ui/textarea";
@@ -22,6 +23,11 @@ export function ChannelCanvas({
}: ChannelCanvasProps) {
const canvasQuery = useCanvasQuery(channelId, channelId !== null);
const setCanvasMutation = useSetCanvasMutation(channelId);
+ const { channels } = useChannelNavigation();
+ const channelNames = React.useMemo(
+ () => channels.filter((c) => c.channelType !== "dm").map((c) => c.name),
+ [channels],
+ );
const [isEditing, setIsEditing] = React.useState(false);
const [draft, setDraft] = React.useState("");
@@ -109,7 +115,11 @@ export function ChannelCanvas({
className="rounded-2xl border border-border/70 bg-muted/20 px-4 py-3"
data-testid="channel-canvas-content"
>
-
+
) : (
diff --git a/desktop/src/features/forum/ui/ForumThreadPanel.tsx b/desktop/src/features/forum/ui/ForumThreadPanel.tsx
index 3f52dabc76..0d30eee6f3 100644
--- a/desktop/src/features/forum/ui/ForumThreadPanel.tsx
+++ b/desktop/src/features/forum/ui/ForumThreadPanel.tsx
@@ -8,6 +8,7 @@ import {
} from "@/features/profile/lib/identity";
import type { ForumThreadResponse, ThreadReply } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
+import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
import { resolveMentionNames } from "@/shared/lib/resolveMentionNames";
import {
AlertDialog,
@@ -96,11 +97,13 @@ function ReplyRow({
reply,
currentPubkey,
profiles,
+ channelNames,
onDelete,
}: {
reply: ThreadReply;
currentPubkey?: string;
profiles?: UserProfileLookup;
+ channelNames?: string[];
onDelete?: (eventId: string) => void;
}) {
const [isDeleteOpen, setIsDeleteOpen] = React.useState(false);
@@ -163,6 +166,7 @@ function ReplyRow({
(null);
const [isDeletePostOpen, setIsDeletePostOpen] = React.useState(false);
+ const { channels } = useChannelNavigation();
+ const channelNames = React.useMemo(
+ () => channels.filter((c) => c.channelType !== "dm").map((c) => c.name),
+ [channels],
+ );
if (isLoading || !thread) {
return (
@@ -292,7 +301,11 @@ export function ForumThreadPanel({
) : null}
-
+
@@ -306,6 +319,7 @@ export function ForumThreadPanel({
{replies.map((reply) => (
("");
const latestCursorRef = React.useRef(0);
+ /** Channel names (original casing) for overlay highlighting. */
+ const knownChannelNames = React.useMemo(
+ () => channels.filter((ch) => ch.channelType !== "dm").map((ch) => ch.name),
+ [channels],
+ );
+
+ /** Lower-cased channel names for case-insensitive prefix matching. */
+ const knownNamesLower = React.useMemo(
+ () => knownChannelNames.map((n) => n.toLowerCase()),
+ [knownChannelNames],
+ );
+
+ const knownNamesLowerRef = React.useRef(knownNamesLower);
+
+ // Keep the known-names ref in sync so the debounced callback never reads stale data.
+ React.useEffect(() => {
+ knownNamesLowerRef.current = knownNamesLower;
+ }, [knownNamesLower]);
+
// Clean up pending timeout on unmount
React.useEffect(() => {
return () => {
@@ -106,9 +111,11 @@ export function useChannelLinks() {
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
- const channel = detectChannelQuery(
+ const channel = detectPrefixQuery(
+ "#",
latestValueRef.current,
latestCursorRef.current,
+ knownNamesLowerRef.current,
);
if (channel) {
setChannelQuery(channel.query);
@@ -189,6 +196,7 @@ export function useChannelLinks() {
handleChannelKeyDown,
insertChannel,
isChannelOpen,
+ knownChannelNames,
updateChannelQuery,
};
}
diff --git a/desktop/src/features/messages/lib/useMentions.ts b/desktop/src/features/messages/lib/useMentions.ts
index 1e6a5a89a8..66d1f48677 100644
--- a/desktop/src/features/messages/lib/useMentions.ts
+++ b/desktop/src/features/messages/lib/useMentions.ts
@@ -3,57 +3,11 @@ import * as React from "react";
import { useManagedAgentsQuery } from "@/features/agents/hooks";
import { useChannelMembersQuery } from "@/features/channels/hooks";
import type { MentionSuggestion } from "@/features/messages/ui/MentionAutocomplete";
+import { detectPrefixQuery } from "@/shared/lib/detectPrefixQuery";
import { escapeRegExp } from "@/shared/lib/mentionPattern";
const MENTION_DEBOUNCE_MS = 120;
-function detectMentionQuery(
- value: string,
- cursorPosition: number,
- knownNamesLower: string[],
-): { query: string; startIndex: number } | null {
- const beforeCursor = value.slice(0, cursorPosition);
-
- // Fast path: single-word mention query (no spaces after @)
- const simpleMatch = beforeCursor.match(/(?:^|[\s])@([^\s]*)$/);
- if (simpleMatch) {
- const query = simpleMatch[1];
- const startIndex = beforeCursor.length - query.length - 1; // -1 for @
- return { query, startIndex };
- }
-
- // Multi-word path: scan backwards for an `@` and check if the text between
- // `@` and the cursor is a prefix of any known multi-word display name.
- const scanStart = Math.max(0, beforeCursor.length - 80);
- for (let i = beforeCursor.length - 1; i >= scanStart; i--) {
- const ch = beforeCursor[i];
- if (ch === "@") {
- // Ensure `@` is at start or preceded by whitespace
- if (i > 0 && !/\s/.test(beforeCursor[i - 1])) {
- continue;
- }
- const candidate = beforeCursor.slice(i + 1);
- if (candidate.length === 0) {
- break;
- }
- const lowerCandidate = candidate.toLowerCase();
- const isPrefix = knownNamesLower.some((name) =>
- name.startsWith(lowerCandidate),
- );
- if (isPrefix) {
- return { query: candidate, startIndex: i };
- }
- break;
- }
- // Stop scanning if we hit a newline — mentions don't span lines
- if (ch === "\n") {
- break;
- }
- }
-
- return null;
-}
-
export function useMentions(channelId: string | null) {
const [mentionQuery, setMentionQuery] = React.useState(null);
const [mentionStartIndex, setMentionStartIndex] = React.useState(0);
@@ -88,7 +42,7 @@ export function useMentions(channelId: string | null) {
return names;
}, [members, managedAgentNamesByPubkey]);
- /** Lower-cased version of knownNames, used for case-insensitive prefix matching in detectMentionQuery. */
+ /** Lower-cased version of knownNames, used for case-insensitive prefix matching. */
const knownNamesLower = React.useMemo(
() => knownNames.map((n) => n.toLowerCase()),
[knownNames],
@@ -190,7 +144,8 @@ export function useMentions(channelId: string | null) {
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
- const mention = detectMentionQuery(
+ const mention = detectPrefixQuery(
+ "@",
latestValueRef.current,
latestCursorRef.current,
knownNamesLowerRef.current,
diff --git a/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx b/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx
index a66b96f25f..c0d61da454 100644
--- a/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx
+++ b/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx
@@ -1,17 +1,19 @@
import React from "react";
-import { buildMentionPattern } from "@/shared/lib/mentionPattern";
+import { buildPrefixPattern } from "@/shared/lib/mentionPattern";
type Segment = { type: "text" | "mention" | "channel"; value: string };
-const CHANNEL_RE_PART = "#[a-zA-Z0-9][\\w-]*";
-
/**
* Extends the shared mention pattern to also match `#channel-name` references.
*/
-function buildOverlayPattern(mentionNames: string[]): RegExp {
- const mentionSource = buildMentionPattern(mentionNames).source;
- return new RegExp(`${mentionSource}|${CHANNEL_RE_PART}`, "g");
+function buildOverlayPattern(
+ mentionNames: string[],
+ channelNames: string[],
+): RegExp {
+ const mentionSource = buildPrefixPattern("@", mentionNames).source;
+ const channelSource = buildPrefixPattern("#", channelNames).source;
+ return new RegExp(`${mentionSource}|${channelSource}`, "gi");
}
function parseSegments(text: string, pattern: RegExp): Segment[] {
@@ -41,6 +43,7 @@ function parseSegments(text: string, pattern: RegExp): Segment[] {
}
type ComposerMentionOverlayProps = {
+ channelNames: string[];
content: string;
mentionNames: string[];
scrollTop: number;
@@ -54,13 +57,14 @@ type ComposerMentionOverlayProps = {
*/
export const ComposerMentionOverlay = React.memo(
function ComposerMentionOverlay({
+ channelNames,
content,
mentionNames,
scrollTop,
}: ComposerMentionOverlayProps) {
const pattern = React.useMemo(
- () => buildOverlayPattern(mentionNames),
- [mentionNames],
+ () => buildOverlayPattern(mentionNames, channelNames),
+ [mentionNames, channelNames],
);
// Memoize regex parsing so it only re-runs when content or the mention
// pattern actually changes — avoids expensive matchAll on every render.
diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx
index 3179d07dce..6aff994a42 100644
--- a/desktop/src/features/messages/ui/MessageComposer.tsx
+++ b/desktop/src/features/messages/ui/MessageComposer.tsx
@@ -586,6 +586,7 @@ export function MessageComposer({
className="pointer-events-none absolute inset-0 overflow-hidden"
>
channels.filter((c) => c.channelType !== "dm").map((c) => c.name),
+ [channels],
+ );
+
const visibleDepth = Math.min(message.depth, 6);
const indentPx = visibleDepth * 28;
const initials = message.author
@@ -91,6 +98,7 @@ export const MessageRow = React.memo(
default:
return (
{
+ // biome-ignore lint/suspicious/noExplicitAny: building mdast-compatible nodes
+ [key: string]: any;
+};
+
+/**
+ * Create a remark plugin that walks the tree, finds regex matches in text
+ * nodes, and replaces each match with a node produced by `buildNode`.
+ */
+export function createRemarkPrefixPlugin(
+ pattern: RegExp,
+ buildNode: NodeBuilder,
+) {
+ return (
+ // biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available
+ tree: any,
+ ) => {
+ walkChildren(tree, pattern, buildNode);
+ };
+}
+
+// biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available
+function walkChildren(node: any, pattern: RegExp, buildNode: NodeBuilder) {
+ if (!node?.children || !Array.isArray(node.children)) {
+ return;
+ }
+
+ for (let i = node.children.length - 1; i >= 0; i--) {
+ const child = node.children[i];
+
+ if (child.type === "text") {
+ const parts = splitByPattern(child.value, pattern, buildNode);
+ if (parts.length > 1) {
+ node.children.splice(i, 1, ...parts);
+ }
+ } else {
+ walkChildren(child, pattern, buildNode);
+ }
+ }
+}
+
+function splitByPattern(text: string, pattern: RegExp, buildNode: NodeBuilder) {
+ // Reset lastIndex — the pattern is reused across text nodes with the `g` flag
+ pattern.lastIndex = 0;
+ // biome-ignore lint/suspicious/noExplicitAny: building mdast-compatible nodes
+ const parts: any[] = [];
+ let lastIndex = 0;
+ let match: RegExpExecArray | null = null;
+
+ while (true) {
+ match = pattern.exec(text);
+ if (!match) {
+ break;
+ }
+
+ if (match.index > lastIndex) {
+ parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
+ }
+
+ parts.push(buildNode(match[0]));
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ if (parts.length === 0) {
+ return [{ type: "text", value: text }];
+ }
+
+ if (lastIndex < text.length) {
+ parts.push({ type: "text", value: text.slice(lastIndex) });
+ }
+
+ return parts;
+}
diff --git a/desktop/src/shared/lib/detectPrefixQuery.ts b/desktop/src/shared/lib/detectPrefixQuery.ts
new file mode 100644
index 0000000000..fd7e20214b
--- /dev/null
+++ b/desktop/src/shared/lib/detectPrefixQuery.ts
@@ -0,0 +1,64 @@
+/**
+ * Shared helper for detecting prefix-based queries (e.g. @mention, #channel)
+ * within a text input, used by both useMentions and useChannelLinks hooks.
+ */
+
+/**
+ * Detect whether the user is typing a prefix-based query (e.g. @name or #channel)
+ * at the current cursor position.
+ *
+ * @param prefix - The trigger character (e.g. "@" or "#")
+ * @param value - The full text content
+ * @param cursorPosition - The current cursor position in the text
+ * @param knownNamesLower - Lower-cased known names for multi-word prefix matching
+ * @returns The query string and start index, or null if no query is detected
+ */
+export function detectPrefixQuery(
+ prefix: string,
+ value: string,
+ cursorPosition: number,
+ knownNamesLower: string[],
+): { query: string; startIndex: number } | null {
+ const beforeCursor = value.slice(0, cursorPosition);
+
+ // Fast path: single-word query (no spaces after the prefix)
+ const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const simplePattern = new RegExp(`(?:^|[\\s])${escapedPrefix}([^\\s]*)$`);
+ const simpleMatch = beforeCursor.match(simplePattern);
+ if (simpleMatch) {
+ const query = simpleMatch[1];
+ const startIndex = beforeCursor.length - query.length - 1; // -1 for prefix
+ return { query, startIndex };
+ }
+
+ // Multi-word path: scan backwards for the prefix and check if the text between
+ // the prefix and the cursor is a prefix of any known multi-word name.
+ const scanStart = Math.max(0, beforeCursor.length - 80);
+ for (let i = beforeCursor.length - 1; i >= scanStart; i--) {
+ const ch = beforeCursor[i];
+ if (ch === prefix) {
+ // Ensure prefix is at start or preceded by whitespace
+ if (i > 0 && !/\s/.test(beforeCursor[i - 1])) {
+ continue;
+ }
+ const candidate = beforeCursor.slice(i + 1);
+ if (candidate.length === 0) {
+ break;
+ }
+ const lowerCandidate = candidate.toLowerCase();
+ const isPrefix = knownNamesLower.some((name) =>
+ name.startsWith(lowerCandidate),
+ );
+ if (isPrefix) {
+ return { query: candidate, startIndex: i };
+ }
+ break;
+ }
+ // Stop scanning if we hit a newline
+ if (ch === "\n") {
+ break;
+ }
+ }
+
+ return null;
+}
diff --git a/desktop/src/shared/lib/mentionPattern.ts b/desktop/src/shared/lib/mentionPattern.ts
index c4d8e4eaf2..ea93a5b125 100644
--- a/desktop/src/shared/lib/mentionPattern.ts
+++ b/desktop/src/shared/lib/mentionPattern.ts
@@ -5,19 +5,36 @@ export function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+/**
+ * Build a regex that matches a given prefix followed by known multi-word names
+ * (longest-first to avoid partial matches), then falling back to prefix + \S+.
+ */
+export function buildPrefixPattern(
+ prefix: string,
+ knownNames: string[],
+): RegExp {
+ const sorted = [...new Set(knownNames)]
+ .filter((name) => name.trim().length > 0)
+ .sort((a, b) => b.length - a.length);
+
+ const escapedPrefix = escapeRegExp(prefix);
+
+ if (sorted.length === 0) {
+ return new RegExp(`${escapedPrefix}\\S+`, "gi");
+ }
+
+ const nameAlternatives = sorted.map((name) => escapeRegExp(name)).join("|");
+ const boundary = "(?=[\\s,;.!?:)\\]}]|$)";
+ return new RegExp(
+ `${escapedPrefix}(?:(?:${nameAlternatives})${boundary}|\\S+)`,
+ "gi",
+ );
+}
+
/**
* Build a regex that matches @mentions, trying known multi-word names first
* (longest-first to avoid partial matches), then falling back to @\S+.
*/
export function buildMentionPattern(mentionNames: string[]): RegExp {
- const sorted = [...new Set(mentionNames)]
- .filter((name) => name.trim().length > 0)
- .sort((a, b) => b.length - a.length);
-
- if (sorted.length === 0) {
- return /@\S+/g;
- }
-
- const nameAlternatives = sorted.map((name) => escapeRegExp(name)).join("|");
- return new RegExp(`@(?:${nameAlternatives}|\\S+)`, "g");
+ return buildPrefixPattern("@", mentionNames);
}
diff --git a/desktop/src/shared/lib/remarkChannelLinks.ts b/desktop/src/shared/lib/remarkChannelLinks.ts
index 5c11fd777b..ca1225a435 100644
--- a/desktop/src/shared/lib/remarkChannelLinks.ts
+++ b/desktop/src/shared/lib/remarkChannelLinks.ts
@@ -1,73 +1,34 @@
/**
* Remark plugin that detects #channel-name patterns in text nodes and wraps them
* in custom HAST `channel-link` elements for styled rendering via react-markdown.
+ *
+ * When `channelNames` is provided, multi-word channel names (e.g. "my channel")
+ * are matched first (longest-first to avoid partial matches), then the plugin
+ * falls back to the generic `#\S+` pattern for unknown channels.
*/
-export default function remarkChannelLinks() {
- // biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available
- return (tree: any) => {
- walkChildren(tree);
- };
-}
-// biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available
-function walkChildren(node: any) {
- if (!node?.children || !Array.isArray(node.children)) {
- return;
- }
+import { createRemarkPrefixPlugin } from "./createRemarkPrefixPlugin";
+import { buildPrefixPattern } from "./mentionPattern";
- for (let i = node.children.length - 1; i >= 0; i--) {
- const child = node.children[i];
+type RemarkChannelLinksOptions = {
+ channelNames?: string[];
+};
- if (child.type === "text") {
- const parts = splitChannelLinks(child.value);
- if (parts.length > 1) {
- node.children.splice(i, 1, ...parts);
- }
- } else {
- walkChildren(child);
- }
- }
-}
+export default function remarkChannelLinks(
+ options?: RemarkChannelLinksOptions,
+) {
+ const channelPattern = buildPrefixPattern("#", options?.channelNames ?? []);
-function splitChannelLinks(text: string) {
- const channelPattern = /#[a-zA-Z0-9][\w-]*/g;
- // biome-ignore lint/suspicious/noExplicitAny: building mdast-compatible nodes
- const parts: any[] = [];
- let lastIndex = 0;
- let match: RegExpExecArray | null = null;
-
- while (true) {
- match = channelPattern.exec(text);
- if (!match) {
- break;
- }
-
- if (match.index > lastIndex) {
- parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
- }
-
- const channelName = match[0].slice(1);
-
- parts.push({
+ return createRemarkPrefixPlugin(channelPattern, (matchText) => {
+ const channelName = matchText.slice(1);
+ return {
type: "channel-link",
- value: match[0],
+ value: matchText,
data: {
hName: "channel-link",
- hChildren: [{ type: "text", value: match[0] }],
+ hChildren: [{ type: "text", value: matchText }],
channelName,
},
- });
-
- lastIndex = match.index + match[0].length;
- }
-
- if (parts.length === 0) {
- return [{ type: "text", value: text }];
- }
-
- if (lastIndex < text.length) {
- parts.push({ type: "text", value: text.slice(lastIndex) });
- }
-
- return parts;
+ };
+ });
}
diff --git a/desktop/src/shared/lib/remarkMentions.ts b/desktop/src/shared/lib/remarkMentions.ts
index 98df839140..b958ea1bb1 100644
--- a/desktop/src/shared/lib/remarkMentions.ts
+++ b/desktop/src/shared/lib/remarkMentions.ts
@@ -7,6 +7,7 @@
* falls back to the generic `@\S+` pattern for unknown mentions.
*/
+import { createRemarkPrefixPlugin } from "./createRemarkPrefixPlugin";
import { buildMentionPattern } from "./mentionPattern";
type RemarkMentionsOptions = {
@@ -16,71 +17,12 @@ type RemarkMentionsOptions = {
export default function remarkMentions(options?: RemarkMentionsOptions) {
const mentionPattern = buildMentionPattern(options?.mentionNames ?? []);
- return (
- // biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available
- tree: any,
- ) => {
- walkChildren(tree, mentionPattern);
- };
-}
-
-// biome-ignore lint/suspicious/noExplicitAny: remark tree types are not available
-function walkChildren(node: any, mentionPattern: RegExp) {
- if (!node?.children || !Array.isArray(node.children)) {
- return;
- }
-
- for (let i = node.children.length - 1; i >= 0; i--) {
- const child = node.children[i];
-
- if (child.type === "text") {
- const parts = splitMentions(child.value, mentionPattern);
- if (parts.length > 1) {
- node.children.splice(i, 1, ...parts);
- }
- } else {
- walkChildren(child, mentionPattern);
- }
- }
-}
-
-function splitMentions(text: string, mentionPattern: RegExp) {
- // Reset lastIndex — the pattern is reused across text nodes with the `g` flag
- mentionPattern.lastIndex = 0;
- // biome-ignore lint/suspicious/noExplicitAny: building mdast-compatible nodes
- const parts: any[] = [];
- let lastIndex = 0;
- let match: RegExpExecArray | null = null;
-
- while (true) {
- match = mentionPattern.exec(text);
- if (!match) {
- break;
- }
-
- if (match.index > lastIndex) {
- parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
- }
-
- parts.push({
- type: "mention",
- value: match[0],
- data: {
- hName: "mention",
- hChildren: [{ type: "text", value: match[0] }],
- },
- });
-
- lastIndex = match.index + match[0].length;
- }
-
- if (parts.length === 0) {
- return [{ type: "text", value: text }];
- }
-
- if (lastIndex < text.length) {
- parts.push({ type: "text", value: text.slice(lastIndex) });
- }
-
- return parts;
+ return createRemarkPrefixPlugin(mentionPattern, (matchText) => ({
+ type: "mention",
+ value: matchText,
+ data: {
+ hName: "mention",
+ hChildren: [{ type: "text", value: matchText }],
+ },
+ }));
}
diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx
index df9bbd16d3..1956a8ada3 100644
--- a/desktop/src/shared/ui/markdown.tsx
+++ b/desktop/src/shared/ui/markdown.tsx
@@ -11,6 +11,7 @@ import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext"
import type { Channel } from "@/shared/api/types";
type MarkdownProps = {
+ channelNames?: string[];
className?: string;
compact?: boolean;
content: string;
@@ -192,6 +193,7 @@ function shallowArrayEqual(a?: string[], b?: string[]): boolean {
}
function MarkdownInner({
+ channelNames,
className,
compact = false,
content,
@@ -210,6 +212,17 @@ function MarkdownInner({
[variant, channels, onOpenChannel],
);
+ // biome-ignore lint/suspicious/noExplicitAny: PluggableList type not directly importable
+ const remarkPlugins = React.useMemo(
+ () => [
+ remarkGfm,
+ remarkBreaks,
+ [remarkMentions, { mentionNames }],
+ [remarkChannelLinks, { channelNames }],
+ ],
+ [mentionNames, channelNames],
+ );
+
let processedContent = content;
if (/^(?:\s{2}\n)+/.test(content)) {
@@ -231,15 +244,7 @@ function MarkdownInner({
className,
)}
>
-
+
{processedContent}
@@ -253,7 +258,8 @@ export const Markdown = React.memo(
prev.className === next.className &&
prev.compact === next.compact &&
prev.tight === next.tight &&
- shallowArrayEqual(prev.mentionNames, next.mentionNames),
+ shallowArrayEqual(prev.mentionNames, next.mentionNames) &&
+ shallowArrayEqual(prev.channelNames, next.channelNames),
);
Markdown.displayName = "Markdown";