Skip to main content
Version: Latest

Mobile Mentions — Integration Guide

For mobile development. Everything needed to surface @username autocomplete, render mention highlights, read a user's mention inbox, and handle mention push notifications. No backend changes needed on your side.

  • API base URL (local): http://localhost:3001
  • Auth: Authorization: Bearer <accessToken> on every request.
  • Send X-Client-Type: mobile on every request.

Short version — just the calls

1. GET /api/users/mention-search?q=al&context=conversation:abc
→ autocomplete picker results
2. GET /api/users/me/mentions → mention inbox (cursor-paginated)
3. POST /api/chat/conversations/:id/messages }
POST /api/posts } — mentions auto-detected
POST /api/posts/:postId/comments } from `content` field

Mentions are created server-side — you never POST a list of mentioned user IDs. Write the raw content string; the backend parses @username tokens, resolves them to user IDs, persists Mention rows, and fires notifications.


1.1 — Endpoint

GET /api/users/mention-search?q=al&context=conversation:abc&limit=8

Auth: Required. Rate limit: 30 requests / 10 seconds per user.

ParamTypeRequiredNotes
qstringyes1–20 chars, [A-Za-z0-9_] only. Server normalizes before lookup.
limitintno1–10. Default 8.
contextstringnopost:<id> | comment:<id> | conversation:<id> — boosts relevant users.

Response:

{
"success": true,
"data": [
{
"id": "usr_01H...",
"username": "Alice",
"displayName": "Alice Smith",
"avatarUrl": "https://...",
"youFollow": true,
"followsYou": false,
"inContext": true, // in the conversation / post's comment thread
},
],
}

Results are pre-ranked server-side:

SignalWeight
Candidate is in the context boost set+8
You follow the candidate+4
Candidate follows you+2
Username (normalized) starts with q+1

Tie-break: alphabetical username. You do not need to sort client-side.

1.2 — When to fire

Trigger when the user types @ followed by at least one word character in a text input. The substring between the @ and the cursor is q.

"hey @al" → q = "al"
"hey @Alice" → q = "Alice"

Stop firing and close the picker when:

  • User taps outside the picker
  • User presses space or newline after the @ token
  • User backspaces past the @
  • User selects a result

1.3 — Debounce + cancellation

let abortController: AbortController | null = null;

async function fetchMentionSuggestions(q: string, context?: string) {
abortController?.abort();
abortController = new AbortController();

// Debounce: wait 200ms before firing
await sleep(200);
if (abortController.signal.aborted) return;

const params = new URLSearchParams({ q, limit: "8" });
if (context) params.set("context", context);

const res = await api.get(`/users/mention-search?${params}`, {
signal: abortController.signal,
});
return res.data.data;
}
  • Debounce 200ms — avoids one request per keystroke.
  • Cancel in-flight request on next keystroke (AbortController).
  • Cache results keyed by (q, context) for the picker session lifetime.
  • Fire on q.length >= 1. Server enforces; client just avoids empty calls.

1.4 — Picker selection

When the user taps a result, replace the @<typed> token with @<username> (preserving the chosen user's casing from username field) and append a space. Submit the raw text to the entity create endpoint — never send a separate list of mentioned user IDs.

function insertMention(
text: string,
atIndex: number,
username: string,
): string {
// atIndex = position of the @ that opened the picker
// replace everything from @ to cursor with @username + space
const before = text.slice(0, atIndex);
const after = text.slice(getCursorPos()); // text after current cursor
return `${before}@${username} ${after}`;
}

2 — Mention inbox

2.1 — Endpoint

GET /api/users/me/mentions
GET /api/users/me/mentions?cursor=m_01G...&limit=20

Returns mentions where the authenticated user is the mentioned user. Ordered by createdAt DESC (newest first). Default limit 20, max 50.

Response:

{
"success": true,
"data": {
"items": [
{
"id": "m_01H...",
"entityType": "POST", // POST | COMMENT | MESSAGE
"entityId": "p_01H...",
"usernameSnapshot": "alice", // your username at time of mention
"startOffset": 4, // UTF-16 offset of @ in content
"endOffset": 11,
"createdAt": "2026-05-07T10:00:00.000Z",
"mentionerUser": {
"id": "usr_b",
"username": "Bob",
"profile": { "displayName": "Bob", "avatarUrl": null },
},
"entity": {
"id": "p_01H...",
"content": "hey @alice check this",
},
},
],
"nextCursor": "m_01G...", // null on final page
},
}

Pass nextCursor as ?cursor= for the next page. Stop when null.

2.2 — Rendering the inbox row

[Avatar] Bob mentioned you in a post
"hey @alice check this" ← entity.content, truncate ~80 chars
2 hours ago
  • entityType === "POST" → deep-link to /posts/<entityId>
  • entityType === "COMMENT" → deep-link to the parent post + scroll to comment
  • entityType === "MESSAGE" → deep-link to /chat/<conversationId> (use the data.conversationId from the push payload — the inbox row doesn't carry it directly, so resolve via the chat screen)

2.3 — Highlight the mention in content

Use startOffset / endOffset (UTF-16 code-unit offsets) to slice the @username token directly:

function highlightMentions(
content: string,
startOffset: number,
endOffset: number,
) {
const before = content.slice(0, startOffset);
const mention = content.slice(startOffset, endOffset); // "@alice"
const after = content.slice(endOffset);
return `${before}<b>${mention}</b>${after}`;
}

If offsets are null (legacy rows), fall back to regex highlight:

const MENTION_REGEX = /(?<![a-zA-Z0-9_])@([a-zA-Z0-9_]{3,20})/g;

3 — How mentions are created (you don't do anything extra)

Whenever you POST a content field to any of these endpoints, the backend auto-parses @username tokens, resolves them, and creates Mention rows:

EndpointMention context
POST /api/postsentityType: POST
POST /api/posts/:postId/commentsentityType: COMMENT
POST /api/chat/conversations/:id/messagesentityType: MESSAGE (group chats only)

In DM chats: mention parsing runs but notifications are suppressed — DM recipients already get a message notification on every send. No duplicate.

Restrictions:

  • A user cannot mention themselves (silently ignored).
  • Inactive / deleted users are silently ignored.
  • In group chats, only current participants can be mentioned.
  • Same @username repeated in one message = one Mention row, one notification.

4 — Push notifications for mentions

When someone mentions you in a group chat or post/comment, you receive a mention push notification. The backend suppresses the generic message notification for that user to avoid double-notifying.

4.1 — Push payload shape

{
"notification": {
"title": "Bob mentioned you in a post", // or "chat" / "comment"
"body": "hey @alice check this", // first 100 chars of content
},
"data": {
"entityType": "post", // "post" | "comment" | "message"
"entityId": "p_01H...",
"actorId": "usr_b",
"actorName": "Bob",
"webPath": "/posts/p_01H...",
"deepLink": "ksn://posts/p_01H...",
// For comment mentions:
"postId": "p_parent_01H...",
// For message mentions:
"conversationId": "cnv_01H...",
},
}
function handleMentionPush(data: PushData) {
switch (data.entityType) {
case "post":
navigate(`/posts/${data.entityId}`);
break;
case "comment":
navigate(`/posts/${data.postId}`, { scrollTo: data.entityId });
break;
case "message":
navigate(`/chat/${data.conversationId}`, { highlight: data.entityId });
break;
}
}

4.3 — Notification preferences

Users can mute mention notifications per channel:

GET /api/notifications/preferences
PUT /api/notifications/preferences
{
"preferences": [
{ "type": "mention", "pushEnabled": true, "inAppEnabled": true, "emailEnabled": false }
]
}

Default: push on, in-app on, email off. Server gates fan-out by these prefs — no client-side suppression needed.


5 — Rendering @mentions in content

Anywhere you display content from posts, comments, or chat messages, apply the mention renderer:

const MENTION_REGEX = /(?<![a-zA-Z0-9_])@([a-zA-Z0-9_]{3,20})/g;

function renderContent(content: string): React.ReactNode {
const parts: React.ReactNode[] = [];
let lastIndex = 0;

for (const match of content.matchAll(MENTION_REGEX)) {
const [full, username] = match;
const start = match.index!;

if (start > lastIndex) {
parts.push(content.slice(lastIndex, start));
}
parts.push(
<MentionChip key={start} username={username} /> // tappable, links to profile
);
lastIndex = start + full.length;
}

parts.push(content.slice(lastIndex));
return <>{parts}</>;
}

MentionChip should navigate to GET /api/users/:username profile on tap. Use mentionedUserId (from Mention rows) as the durable ID — username may change, but the ID is permanent.


6 — Error codes

CodeMeaning
400q param missing, out of range, or invalid chars
401Missing / expired Authorization token
403Not authenticated
429Mention search rate limit (30 req / 10s). Back off autocomplete; show "Slow down" if persistent.

6.1 — Autocomplete error UX

ScenarioUX
429Close picker or show "Slow down" badge. Retry after Retry-After header.
Empty resultsShow "No matches"
Network errorSilent — do not block typing
Aborted requestSilent — next debounce cycle will fire

7 — Edge cases reference

CaseBehavior
User mentions themselvesSilently ignored — no row, no notif
@alice appears twice in one messageOne Mention row, one notification (unique constraint)
Mentioned user is inactiveSilently ignored — no row, no notif
Username changed after mention was savedMention.mentionedUserId still routes correctly; usernameSnapshot shows original text
User deletes accountMention row auto-deleted (DB cascade). Render gracefully.
Edit removes a mentionMention row deleted; push already delivered is NOT retracted
@user in a DMMention row created but notification suppressed (DM already notifies)
email@domain.com in contentRegex skips — lookbehind prevents @domain from matching

8 — Quick implementation checklist

Work in this order:

  1. Autocomplete trigger — detect @ in <TextInput>. Extract query substring. Call mention search with context param (pass conversation:<id> or post:<id> if in context).
  2. Debounce + cancel — 200ms debounce, AbortController per keystroke.
  3. Picker UI — render username + displayName + avatarUrl. Tap inserts @username into input, closes picker.
  4. Submit raw text — POST content as-is. Do not modify or strip @ tokens before sending.
  5. Render mentions in feed / chat — apply regex highlight across all content display sites. MentionChip links to user profile.
  6. Mention inbox screen — GET /api/users/me/mentions, cursor-paginate, deep-link on row tap.
  7. Push handling — register listener for data.entityType === "mention". Deep-link to entityType (post / comment / chat).
  8. Notification prefs — expose mention toggle in settings (push / in-app).
⤓ Download .md