@antzsoft/chat-core 1.3.8 → 1.4.0
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 +79 -1
- package/dist/index.cjs +96 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -3
- package/dist/index.d.ts +63 -3
- package/dist/index.js +90 -3
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{storage-BJvQhWxC.d.cts → storage-C_b-SFFH.d.cts} +21 -1
- package/dist/{storage-BJvQhWxC.d.ts → storage-C_b-SFFH.d.ts} +21 -1
- package/docs/integration-guide.html +37 -3
- package/package.json +1 -1
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.
|
|
@@ -1519,6 +1525,7 @@ var _getToken = null;
|
|
|
1519
1525
|
var _userId;
|
|
1520
1526
|
var _tenantId;
|
|
1521
1527
|
var _config2 = null;
|
|
1528
|
+
var _preservingSession = false;
|
|
1522
1529
|
function setStatus(s) {
|
|
1523
1530
|
_status = s;
|
|
1524
1531
|
_statusListeners.forEach((l) => l(s));
|
|
@@ -1670,7 +1677,7 @@ async function _doConnect(config, getToken) {
|
|
|
1670
1677
|
_socket.on("connect", () => setStatus("connected"));
|
|
1671
1678
|
_socket.on("disconnect", () => {
|
|
1672
1679
|
setStatus("disconnected");
|
|
1673
|
-
clearTransitSession();
|
|
1680
|
+
if (!_preservingSession) clearTransitSession();
|
|
1674
1681
|
});
|
|
1675
1682
|
_socket.on("connect_error", (err) => {
|
|
1676
1683
|
console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
|
|
@@ -1773,7 +1780,13 @@ function reconnectSocket(token, userId, tenantId) {
|
|
|
1773
1780
|
...tenantId && { tenantId },
|
|
1774
1781
|
transitSessionId: existing.sessionId
|
|
1775
1782
|
};
|
|
1776
|
-
|
|
1783
|
+
_preservingSession = true;
|
|
1784
|
+
try {
|
|
1785
|
+
if (_socket.connected) _socket.disconnect();
|
|
1786
|
+
_socket.connect();
|
|
1787
|
+
} finally {
|
|
1788
|
+
_preservingSession = false;
|
|
1789
|
+
}
|
|
1777
1790
|
return;
|
|
1778
1791
|
}
|
|
1779
1792
|
if (_getToken) {
|
|
@@ -1945,6 +1958,80 @@ var socketEmit = {
|
|
|
1945
1958
|
// src/index.ts
|
|
1946
1959
|
init_chat_store();
|
|
1947
1960
|
|
|
1961
|
+
// src/types/index.ts
|
|
1962
|
+
var MENTION_ALL_ID = "all";
|
|
1963
|
+
|
|
1964
|
+
// src/utils/mentions.ts
|
|
1965
|
+
var MENTION_TOKEN_SOURCE = "@\\[([^\\]]+)\\]\\((all|[a-fA-F0-9]{24})\\)";
|
|
1966
|
+
function sanitizeDisplayName(name) {
|
|
1967
|
+
return name.replace(/[\[\]()]/g, "").trim() || "user";
|
|
1968
|
+
}
|
|
1969
|
+
function parseMentions(text) {
|
|
1970
|
+
if (!text) return [];
|
|
1971
|
+
const re = new RegExp(MENTION_TOKEN_SOURCE, "g");
|
|
1972
|
+
const out = [];
|
|
1973
|
+
let m;
|
|
1974
|
+
while ((m = re.exec(text)) !== null) {
|
|
1975
|
+
out.push({
|
|
1976
|
+
id: m[2],
|
|
1977
|
+
displayName: m[1],
|
|
1978
|
+
start: m.index,
|
|
1979
|
+
end: m.index + m[0].length
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
return out;
|
|
1983
|
+
}
|
|
1984
|
+
function renderMentionParts(text, resolveName) {
|
|
1985
|
+
if (!text) return [];
|
|
1986
|
+
const mentions = parseMentions(text);
|
|
1987
|
+
if (mentions.length === 0) return [{ type: "text", text }];
|
|
1988
|
+
const parts = [];
|
|
1989
|
+
let cursor = 0;
|
|
1990
|
+
for (const mn of mentions) {
|
|
1991
|
+
if (mn.start > cursor) {
|
|
1992
|
+
parts.push({ type: "text", text: text.slice(cursor, mn.start) });
|
|
1993
|
+
}
|
|
1994
|
+
const displayName = resolveName ? resolveName(mn.id, mn.displayName) : mn.displayName;
|
|
1995
|
+
parts.push({ type: "mention", id: mn.id, displayName });
|
|
1996
|
+
cursor = mn.end;
|
|
1997
|
+
}
|
|
1998
|
+
if (cursor < text.length) {
|
|
1999
|
+
parts.push({ type: "text", text: text.slice(cursor) });
|
|
2000
|
+
}
|
|
2001
|
+
return parts;
|
|
2002
|
+
}
|
|
2003
|
+
function buildMentionText(segments) {
|
|
2004
|
+
let text = "";
|
|
2005
|
+
const ids = [];
|
|
2006
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2007
|
+
for (const seg of segments) {
|
|
2008
|
+
if (typeof seg === "string") {
|
|
2009
|
+
text += seg;
|
|
2010
|
+
} else {
|
|
2011
|
+
text += `@[${sanitizeDisplayName(seg.displayName)}](${seg.id})`;
|
|
2012
|
+
if (!seen.has(seg.id)) {
|
|
2013
|
+
seen.add(seg.id);
|
|
2014
|
+
ids.push(seg.id);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return { text, mentions: ids };
|
|
2019
|
+
}
|
|
2020
|
+
function extractMentionIds(text) {
|
|
2021
|
+
const ids = [];
|
|
2022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2023
|
+
for (const mn of parseMentions(text)) {
|
|
2024
|
+
if (!seen.has(mn.id)) {
|
|
2025
|
+
seen.add(mn.id);
|
|
2026
|
+
ids.push(mn.id);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return ids;
|
|
2030
|
+
}
|
|
2031
|
+
function isMentionAll(mentions) {
|
|
2032
|
+
return !!mentions?.includes(MENTION_ALL_ID);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1948
2035
|
// src/client-facade.ts
|
|
1949
2036
|
var AntzChatClient = class {
|
|
1950
2037
|
constructor(rawConfig) {
|
|
@@ -2003,8 +2090,10 @@ var AntzChatClient = class {
|
|
|
2003
2090
|
AntzChatPermissionError,
|
|
2004
2091
|
AntzChatServerError,
|
|
2005
2092
|
AntzChatValidationError,
|
|
2093
|
+
MENTION_ALL_ID,
|
|
2006
2094
|
appConfigApi,
|
|
2007
2095
|
authApi,
|
|
2096
|
+
buildMentionText,
|
|
2008
2097
|
connectSocket,
|
|
2009
2098
|
conversationsApi,
|
|
2010
2099
|
createAuthStore,
|
|
@@ -2013,6 +2102,7 @@ var AntzChatClient = class {
|
|
|
2013
2102
|
devicesApi,
|
|
2014
2103
|
disconnectSocket,
|
|
2015
2104
|
encryptPayload,
|
|
2105
|
+
extractMentionIds,
|
|
2016
2106
|
fetchServerKeys,
|
|
2017
2107
|
generateEphemeralKey,
|
|
2018
2108
|
getApiClient,
|
|
@@ -2024,14 +2114,17 @@ var AntzChatClient = class {
|
|
|
2024
2114
|
getSocketStatus,
|
|
2025
2115
|
initApiClient,
|
|
2026
2116
|
initAuthStore,
|
|
2117
|
+
isMentionAll,
|
|
2027
2118
|
isTransitEnvelope,
|
|
2028
2119
|
messagesApi,
|
|
2029
2120
|
normalizeAxiosError,
|
|
2030
2121
|
normalizeConversation,
|
|
2031
2122
|
onSocketStatus,
|
|
2123
|
+
parseMentions,
|
|
2032
2124
|
performHandshake,
|
|
2033
2125
|
reconnectSocket,
|
|
2034
2126
|
refreshSocketAuth,
|
|
2127
|
+
renderMentionParts,
|
|
2035
2128
|
resetAuthStore,
|
|
2036
2129
|
resolveConfig,
|
|
2037
2130
|
resolveSystemMessageText,
|