@antzsoft/chat-core 1.3.7 → 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
  /**
@@ -2656,6 +2717,7 @@ Returned by `messagesApi.getReceipts()`. Use as the initial load for a "Read by
2656
2717
  ```typescript
2657
2718
  interface MessageReceiptEntry {
2658
2719
  userId: string;
2720
+ externalId?: string; // external system user ID (non-builtin modes)
2659
2721
  displayName: string;
2660
2722
  avatarUrl?: string;
2661
2723
  }
@@ -2685,6 +2747,7 @@ Returned by `messagesApi.getReactions()`. Use as the data source for a reactions
2685
2747
  ```typescript
2686
2748
  interface ReactionUser {
2687
2749
  userId: string;
2750
+ externalId?: string; // external system user ID (non-builtin modes)
2688
2751
  displayName: string;
2689
2752
  avatarUrl?: string;
2690
2753
  }
@@ -2801,6 +2864,7 @@ interface MessageConfig {
2801
2864
  ```typescript
2802
2865
  interface Participant {
2803
2866
  userId: string;
2867
+ externalId?: string; // external system user ID (non-builtin modes); mirrored on user.externalId
2804
2868
  role: 'admin' | 'member';
2805
2869
  joinedAt: string;
2806
2870
  isActive?: boolean;
@@ -2848,6 +2912,7 @@ interface SendMessagePayload {
2848
2912
  attachments?: SendMessageAttachment[];
2849
2913
  replyTo?: string; // messageId
2850
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
2851
2916
  }
2852
2917
 
2853
2918
  interface SendMessageAttachment {
@@ -2893,6 +2958,7 @@ interface ReactionUpdatedEvent {
2893
2958
  interface TypingIndicatorEvent {
2894
2959
  conversationId: string;
2895
2960
  userId: string;
2961
+ externalId?: string; // external system user ID (non-builtin modes); absent in builtin mode
2896
2962
  username: string;
2897
2963
  displayName: string;
2898
2964
  avatarUrl?: string;
@@ -3056,6 +3122,34 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
3056
3122
 
3057
3123
  ## Changelog
3058
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
+
3141
+ ### v1.3.8
3142
+
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`:
3144
+
3145
+ - **`Participant`** — new top-level `externalId?` field, also mirrored onto `Participant.user.externalId`. (`conversationsApi` + conversation socket events.)
3146
+ - **`Message.sender`** — now includes `externalId` (it is a `User`, so the field was always in the type; the server now populates it on the embed).
3147
+ - **`ReactionUser`** (`messagesApi.getReactions`) — new `externalId?`.
3148
+ - **`MessageReceiptEntry`** (`messagesApi.getReceipts`) — new `externalId?`.
3149
+ - **`TypingIndicatorEvent`** (socket) — new `externalId?`. Present in non-builtin modes; **absent in builtin mode**, where no external identity exists.
3150
+
3151
+ **Backward compatible, additive-only.** Every field is optional (`externalId?`). An older SDK against this server simply ignores the extra field; this SDK against an older server sees `externalId` as `undefined`. Note `externalId` (a string, the chat user's external identity) is distinct from the pre-existing `Participant`-level `externalUserId` (a number) — both are sent; do not confuse them. Requires server v-with-this-change or newer to be populated; safe to upgrade regardless. **No integration changes required** — read `externalId` where you previously had to look it up separately.
3152
+
3059
3153
  ### v1.3.7
3060
3154
 
3061
3155
  - **Docs only — no code changes.** Brought the docs fully up to date: added the missing
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,
@@ -1105,11 +1111,13 @@ function normalizeParticipant(p) {
1105
1111
  const hasUserDetails = p.displayName || p.username || p.avatarUrl;
1106
1112
  return {
1107
1113
  userId: p.userId,
1114
+ externalId: p.externalId ?? p.user?.externalId,
1108
1115
  role: p.role,
1109
1116
  joinedAt: p.joinedAt,
1110
1117
  isActive: p.isActive,
1111
1118
  user: hasUserDetails ? {
1112
1119
  id: p.userId,
1120
+ externalId: p.externalId,
1113
1121
  tenantId: "",
1114
1122
  email: "",
1115
1123
  username: p.username ?? "",
@@ -1217,7 +1225,7 @@ var conversationsApi = {
1217
1225
  `/conversations/${conversationId}/participants`,
1218
1226
  filter ? { params: { filter } } : void 0
1219
1227
  );
1220
- return data;
1228
+ return (data ?? []).map(normalizeParticipant);
1221
1229
  },
1222
1230
  /**
1223
1231
  * Get unread message count for a single conversation.
@@ -1943,6 +1951,80 @@ var socketEmit = {
1943
1951
  // src/index.ts
1944
1952
  init_chat_store();
1945
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
+
1946
2028
  // src/client-facade.ts
1947
2029
  var AntzChatClient = class {
1948
2030
  constructor(rawConfig) {
@@ -2001,8 +2083,10 @@ var AntzChatClient = class {
2001
2083
  AntzChatPermissionError,
2002
2084
  AntzChatServerError,
2003
2085
  AntzChatValidationError,
2086
+ MENTION_ALL_ID,
2004
2087
  appConfigApi,
2005
2088
  authApi,
2089
+ buildMentionText,
2006
2090
  connectSocket,
2007
2091
  conversationsApi,
2008
2092
  createAuthStore,
@@ -2011,6 +2095,7 @@ var AntzChatClient = class {
2011
2095
  devicesApi,
2012
2096
  disconnectSocket,
2013
2097
  encryptPayload,
2098
+ extractMentionIds,
2014
2099
  fetchServerKeys,
2015
2100
  generateEphemeralKey,
2016
2101
  getApiClient,
@@ -2022,14 +2107,17 @@ var AntzChatClient = class {
2022
2107
  getSocketStatus,
2023
2108
  initApiClient,
2024
2109
  initAuthStore,
2110
+ isMentionAll,
2025
2111
  isTransitEnvelope,
2026
2112
  messagesApi,
2027
2113
  normalizeAxiosError,
2028
2114
  normalizeConversation,
2029
2115
  onSocketStatus,
2116
+ parseMentions,
2030
2117
  performHandshake,
2031
2118
  reconnectSocket,
2032
2119
  refreshSocketAuth,
2120
+ renderMentionParts,
2033
2121
  resetAuthStore,
2034
2122
  resolveConfig,
2035
2123
  resolveSystemMessageText,