@antzsoft/chat-core 1.3.8 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,8 @@ npm install @antzsoft/chat-core
44
44
  |---|---|
45
45
  | Authentication | Login, register, logout, token refresh (automatic on 401) |
46
46
  | Conversations | List, create (group/DM), update, delete, mute, pin, leave, manage members |
47
- | Messages | Send, edit, delete, react, star, pin, search, paginate |
47
+ | Messages | Send, edit, delete, react, star, pin, search, paginate, @mention |
48
+ | Mentions | Group @mentions with `@all` (admin-gated); token parse/build/render helpers; mention pierces mute |
48
49
  | File uploads | Presigned URL pipeline — request URL → upload binary (multipart POST for S3/local, PUT for Azure) → confirm. Files ≥ 10 MB on S3 or local use chunked multipart (parallel parts → complete). |
49
50
  | Real-time | Socket.IO wrapper — send/receive messages, typing, read receipts, presence |
50
51
  | State management | Zustand auth store (persisted) + chat store (typing users, online status, reply/edit state) |
@@ -778,6 +779,7 @@ interface SendData {
778
779
  attachments?: SendMessageAttachment[];
779
780
  replyTo?: string; // messageId of the message being replied to
780
781
  tempId?: string; // Client-generated ID for optimistic UI
782
+ mentions?: string[]; // Mentioned userIds ('all' for @all); derived from @[name](id) tokens in text
781
783
  }
782
784
 
783
785
  interface SearchParams {
@@ -862,6 +864,64 @@ After the user reads the messages, call `socketEmit.markRead(conversationId)` (s
862
864
 
863
865
  ---
864
866
 
867
+ ### Mentions (`@antzsoft/chat-core` utilities)
868
+
869
+ Group @mentions let a user tag specific members (or everyone via `@all`). A mentioned
870
+ user is notified **even if they muted the group** (the mention pierces mute), while
871
+ everyone else follows normal mute rules. `@all` is server-gated to group **admins**.
872
+
873
+ **Storage model.** A mention is stored inline in the message text as a self-describing
874
+ token — `@[DisplayName](userId)`, and `@[all](all)` for @all — plus a flat
875
+ `mentions: string[]` array on the message (denormalized userIds, `'all'` for @all).
876
+ There are **no character offsets**: the token is self-locating and survives edits, and
877
+ the embedded name is a *fallback* for rendering (the current name is resolved live).
878
+
879
+ ```typescript
880
+ import {
881
+ parseMentions,
882
+ buildMentionText,
883
+ renderMentionParts,
884
+ extractMentionIds,
885
+ isMentionAll,
886
+ MENTION_ALL_ID, // 'all'
887
+ } from '@antzsoft/chat-core';
888
+ ```
889
+
890
+ | Function | Signature | Use |
891
+ |---|---|---|
892
+ | `buildMentionText` | `(segments: MentionSegment[]) => { text; mentions }` | Composer: turn picked members into token text + the id array to send |
893
+ | `parseMentions` | `(text) => ParsedMention[]` | Locate `@[name](id)` tokens (id, displayName, start, end) |
894
+ | `renderMentionParts` | `(text, resolveName?) => MentionPart[]` | Split text into ordered `text`/`mention` parts for rendering |
895
+ | `extractMentionIds` | `(text) => string[]` | Re-derive the id array from tokens (after an edit) |
896
+ | `isMentionAll` | `(mentions?) => boolean` | True when the list targets everyone |
897
+
898
+ ```typescript
899
+ // Compose — from a member picked in your @-autocomplete
900
+ const { text, mentions } = buildMentionText([
901
+ 'Hey ', { id: 'a1b2…', displayName: 'Alice' }, ', please review',
902
+ ]);
903
+ // text → "Hey @[Alice](a1b2…), please review"
904
+ // mentions → ["a1b2…"]
905
+ await messagesApi.send(conversationId, { text, mentions, tempId });
906
+
907
+ // Render — resolve the CURRENT name from your directory; fall back to the token name
908
+ const parts = renderMentionParts(message.content.text, (id, fallbackName) =>
909
+ participants.find((p) => p.userId === id)?.user?.displayName ?? fallbackName,
910
+ );
911
+ // parts: [{type:'text', text:'Hey '}, {type:'mention', id, displayName:'Alice'}, …]
912
+ ```
913
+
914
+ **Name resolution (rename / departed member).** Always prefer the live-resolved name so
915
+ renames show correctly; use the token's embedded name only when the id can't be resolved
916
+ (a member who left, or a client without a directory). This mirrors how WhatsApp/Slack
917
+ resolve mention names at render time.
918
+
919
+ **Picking members.** Source the @-autocomplete from `conversationsApi.getMembers(conversationId)`
920
+ (active members only — never pass `filter`) or the already-normalized
921
+ `conversation.participants`. See the web/RN SDKs for a ready-made composer + renderer.
922
+
923
+ ---
924
+
865
925
  ### Conversations API (`conversationsApi`)
866
926
 
867
927
  ```typescript
@@ -2579,6 +2639,7 @@ interface Message {
2579
2639
  content: MessageContent;
2580
2640
  metadata?: MessageMetadata;
2581
2641
  replyTo?: MessageReplyReference;
2642
+ mentions?: string[]; // v1.3.9+ — mentioned userIds ('all' for @all); denormalized from @[name](id) tokens in content.text
2582
2643
  reactions: MessageReaction[];
2583
2644
  status: 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';
2584
2645
  /**
@@ -2851,6 +2912,7 @@ interface SendMessagePayload {
2851
2912
  attachments?: SendMessageAttachment[];
2852
2913
  replyTo?: string; // messageId
2853
2914
  tempId: string; // client-generated; echoed back in message_ack
2915
+ mentions?: string[]; // mentioned userIds ('all' for @all); derived from @[name](id) tokens in text
2854
2916
  }
2855
2917
 
2856
2918
  interface SendMessageAttachment {
@@ -3060,6 +3122,22 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
3060
3122
 
3061
3123
  ## Changelog
3062
3124
 
3125
+ ### v1.3.9
3126
+
3127
+ - **New: group @mentions.** Tag members in a group message; a mentioned user is notified even if they muted the group (the mention pierces mute). Mentions are stored inline in the message text as self-describing tokens `@[DisplayName](userId)` (and `@[all](all)` for @all), plus a flat, denormalized `mentions: string[]` array on the message for fan-out and "who was mentioned" lookups. No offsets are stored — the token is self-locating and survives edits.
3128
+
3129
+ - **New helpers** (all exported from the package root):
3130
+ - `parseMentions(text)` → `ParsedMention[]` — locate tokens for rendering.
3131
+ - `renderMentionParts(text, resolveName?)` → `MentionPart[]` — split text into ordered text/mention segments; pass `resolveName(id, fallbackName)` to show the *current* name from your directory (handles renames), falling back to the token's embedded name for departed/unresolvable users.
3132
+ - `buildMentionText(segments)` → `{ text, mentions }` — compose token text + the id array from picked members.
3133
+ - `extractMentionIds(text)` — re-derive the id array from tokens (use after an edit; a removed token drops out).
3134
+ - `isMentionAll(mentions)` and the `MENTION_ALL_ID` constant (`'all'`).
3135
+ - New types: `Mention` conventions via `ParsedMention`, `MentionPart`, `MentionSegment`.
3136
+ - **Type changes:** `Message.mentions?: string[]` and `SendMessagePayload.mentions?: string[]` (both optional).
3137
+ - **`conversationsApi.getMembers()`** now normalizes participants so `user.displayName`/`avatarUrl` arrive consistently — a drop-in name source for a mention picker.
3138
+
3139
+ **Backward compatible, additive-only.** Every field is optional. An old client viewing a mention message shows the readable token text (e.g. `@[Alice](…)`) rather than crashing; a new client renders a styled `@Alice`. Requires server support to persist/notify mentions; safe to upgrade regardless. **No integration changes required** unless you are building a mention composer/renderer.
3140
+
3063
3141
  ### v1.3.8
3064
3142
 
3065
3143
  - **`externalId` now surfaced on every embedded user shape, not just the standalone user profile.** Previously `externalId` (the user's ID in your external system, non-builtin modes) was only present on `User` returned from `usersApi.list/getById/updateProfile`. Every *denormalized* place a user appeared — conversation participants, message sender, reaction users, and read/delivery receipts — dropped it, forcing a separate `usersApi.getById()` call to map a chat user back to your own system. Those shapes now carry `externalId`:
package/dist/index.cjs CHANGED
@@ -106,8 +106,10 @@ __export(src_exports, {
106
106
  AntzChatPermissionError: () => AntzChatPermissionError,
107
107
  AntzChatServerError: () => AntzChatServerError,
108
108
  AntzChatValidationError: () => AntzChatValidationError,
109
+ MENTION_ALL_ID: () => MENTION_ALL_ID,
109
110
  appConfigApi: () => appConfigApi,
110
111
  authApi: () => authApi,
112
+ buildMentionText: () => buildMentionText,
111
113
  connectSocket: () => connectSocket,
112
114
  conversationsApi: () => conversationsApi,
113
115
  createAuthStore: () => createAuthStore,
@@ -116,6 +118,7 @@ __export(src_exports, {
116
118
  devicesApi: () => devicesApi,
117
119
  disconnectSocket: () => disconnectSocket,
118
120
  encryptPayload: () => encryptPayload,
121
+ extractMentionIds: () => extractMentionIds,
119
122
  fetchServerKeys: () => fetchServerKeys,
120
123
  generateEphemeralKey: () => generateEphemeralKey,
121
124
  getApiClient: () => getApiClient,
@@ -127,14 +130,17 @@ __export(src_exports, {
127
130
  getSocketStatus: () => getSocketStatus,
128
131
  initApiClient: () => initApiClient,
129
132
  initAuthStore: () => initAuthStore,
133
+ isMentionAll: () => isMentionAll,
130
134
  isTransitEnvelope: () => isTransitEnvelope,
131
135
  messagesApi: () => messagesApi,
132
136
  normalizeAxiosError: () => normalizeAxiosError,
133
137
  normalizeConversation: () => normalizeConversation,
134
138
  onSocketStatus: () => onSocketStatus,
139
+ parseMentions: () => parseMentions,
135
140
  performHandshake: () => performHandshake,
136
141
  reconnectSocket: () => reconnectSocket,
137
142
  refreshSocketAuth: () => refreshSocketAuth,
143
+ renderMentionParts: () => renderMentionParts,
138
144
  resetAuthStore: () => resetAuthStore,
139
145
  resolveConfig: () => resolveConfig,
140
146
  resolveSystemMessageText: () => resolveSystemMessageText,
@@ -1219,7 +1225,7 @@ var conversationsApi = {
1219
1225
  `/conversations/${conversationId}/participants`,
1220
1226
  filter ? { params: { filter } } : void 0
1221
1227
  );
1222
- return data;
1228
+ return (data ?? []).map(normalizeParticipant);
1223
1229
  },
1224
1230
  /**
1225
1231
  * Get unread message count for a single conversation.
@@ -1945,6 +1951,80 @@ var socketEmit = {
1945
1951
  // src/index.ts
1946
1952
  init_chat_store();
1947
1953
 
1954
+ // src/types/index.ts
1955
+ var MENTION_ALL_ID = "all";
1956
+
1957
+ // src/utils/mentions.ts
1958
+ var MENTION_TOKEN_SOURCE = "@\\[([^\\]]+)\\]\\((all|[a-fA-F0-9]{24})\\)";
1959
+ function sanitizeDisplayName(name) {
1960
+ return name.replace(/[\[\]()]/g, "").trim() || "user";
1961
+ }
1962
+ function parseMentions(text) {
1963
+ if (!text) return [];
1964
+ const re = new RegExp(MENTION_TOKEN_SOURCE, "g");
1965
+ const out = [];
1966
+ let m;
1967
+ while ((m = re.exec(text)) !== null) {
1968
+ out.push({
1969
+ id: m[2],
1970
+ displayName: m[1],
1971
+ start: m.index,
1972
+ end: m.index + m[0].length
1973
+ });
1974
+ }
1975
+ return out;
1976
+ }
1977
+ function renderMentionParts(text, resolveName) {
1978
+ if (!text) return [];
1979
+ const mentions = parseMentions(text);
1980
+ if (mentions.length === 0) return [{ type: "text", text }];
1981
+ const parts = [];
1982
+ let cursor = 0;
1983
+ for (const mn of mentions) {
1984
+ if (mn.start > cursor) {
1985
+ parts.push({ type: "text", text: text.slice(cursor, mn.start) });
1986
+ }
1987
+ const displayName = resolveName ? resolveName(mn.id, mn.displayName) : mn.displayName;
1988
+ parts.push({ type: "mention", id: mn.id, displayName });
1989
+ cursor = mn.end;
1990
+ }
1991
+ if (cursor < text.length) {
1992
+ parts.push({ type: "text", text: text.slice(cursor) });
1993
+ }
1994
+ return parts;
1995
+ }
1996
+ function buildMentionText(segments) {
1997
+ let text = "";
1998
+ const ids = [];
1999
+ const seen = /* @__PURE__ */ new Set();
2000
+ for (const seg of segments) {
2001
+ if (typeof seg === "string") {
2002
+ text += seg;
2003
+ } else {
2004
+ text += `@[${sanitizeDisplayName(seg.displayName)}](${seg.id})`;
2005
+ if (!seen.has(seg.id)) {
2006
+ seen.add(seg.id);
2007
+ ids.push(seg.id);
2008
+ }
2009
+ }
2010
+ }
2011
+ return { text, mentions: ids };
2012
+ }
2013
+ function extractMentionIds(text) {
2014
+ const ids = [];
2015
+ const seen = /* @__PURE__ */ new Set();
2016
+ for (const mn of parseMentions(text)) {
2017
+ if (!seen.has(mn.id)) {
2018
+ seen.add(mn.id);
2019
+ ids.push(mn.id);
2020
+ }
2021
+ }
2022
+ return ids;
2023
+ }
2024
+ function isMentionAll(mentions) {
2025
+ return !!mentions?.includes(MENTION_ALL_ID);
2026
+ }
2027
+
1948
2028
  // src/client-facade.ts
1949
2029
  var AntzChatClient = class {
1950
2030
  constructor(rawConfig) {
@@ -2003,8 +2083,10 @@ var AntzChatClient = class {
2003
2083
  AntzChatPermissionError,
2004
2084
  AntzChatServerError,
2005
2085
  AntzChatValidationError,
2086
+ MENTION_ALL_ID,
2006
2087
  appConfigApi,
2007
2088
  authApi,
2089
+ buildMentionText,
2008
2090
  connectSocket,
2009
2091
  conversationsApi,
2010
2092
  createAuthStore,
@@ -2013,6 +2095,7 @@ var AntzChatClient = class {
2013
2095
  devicesApi,
2014
2096
  disconnectSocket,
2015
2097
  encryptPayload,
2098
+ extractMentionIds,
2016
2099
  fetchServerKeys,
2017
2100
  generateEphemeralKey,
2018
2101
  getApiClient,
@@ -2024,14 +2107,17 @@ var AntzChatClient = class {
2024
2107
  getSocketStatus,
2025
2108
  initApiClient,
2026
2109
  initAuthStore,
2110
+ isMentionAll,
2027
2111
  isTransitEnvelope,
2028
2112
  messagesApi,
2029
2113
  normalizeAxiosError,
2030
2114
  normalizeConversation,
2031
2115
  onSocketStatus,
2116
+ parseMentions,
2032
2117
  performHandshake,
2033
2118
  reconnectSocket,
2034
2119
  refreshSocketAuth,
2120
+ renderMentionParts,
2035
2121
  resetAuthStore,
2036
2122
  resolveConfig,
2037
2123
  resolveSystemMessageText,