@pmate/chat-sdk 0.2.3 → 0.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 +29 -0
- package/dist/assets/{ChatInput.stories-pjQKncGw.js → ChatInput.stories-BQcIQDVE.js} +232 -24
- package/dist/assets/{Color-AVL7NMMY-CKkkQz33.js → Color-AVL7NMMY-Q9rY0eHd.js} +1 -1
- package/dist/assets/{DocsRenderer-PQXLIZUC-CVx0v9rK.js → DocsRenderer-PQXLIZUC-B5DlnesB.js} +4 -4
- package/dist/assets/{client-CMlGEQEC.js → client-DTiYOHAb.js} +1 -1
- package/dist/assets/{iframe-Bo_J3UyC.js → iframe-1QrxCBqr.js} +3 -3
- package/dist/assets/{index-D9myAQ7a.js → index-CkqBtKlT.js} +1 -1
- package/dist/assets/{react-18-CVKdgGic.js → react-18-6BKf31wF.js} +1 -1
- package/dist/iframe.html +1 -1
- package/dist/index.html +1 -1
- package/dist/index.json +1 -1
- package/dist/project.json +1 -1
- package/lib/components/ChatInputDesktop.js +146 -141
- package/lib/components/ChatInputMobile.js +12 -6
- package/lib/components/MentionMenu.d.ts +15 -0
- package/lib/components/MentionMenu.js +16 -0
- package/lib/hooks/useMentions.d.ts +38 -0
- package/lib/hooks/useMentions.js +83 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/mentions/model.d.ts +28 -0
- package/lib/mentions/model.js +40 -0
- package/lib/runtime/activity.d.ts +68 -0
- package/lib/runtime/activity.js +121 -0
- package/lib/runtime/chatClient.d.ts +14 -0
- package/lib/runtime/chatClient.js +43 -2
- package/lib/runtime/index.d.ts +1 -0
- package/lib/runtime/index.js +1 -0
- package/lib/runtime/protocol.d.ts +2 -1
- package/lib/types/chatInput.d.ts +4 -0
- package/package.json +16 -17
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Host-owned identity; names need not be unique. */
|
|
2
|
+
export type MentionCandidate = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
avatar?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
};
|
|
8
|
+
/** UTF-16 offsets [start, end) in the sent text, including @. */
|
|
9
|
+
export type ChatMention = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
};
|
|
15
|
+
export type MentionQuery = {
|
|
16
|
+
start: number;
|
|
17
|
+
end: number;
|
|
18
|
+
query: string;
|
|
19
|
+
};
|
|
20
|
+
export type MentionConfig = {
|
|
21
|
+
candidates: MentionCandidate[];
|
|
22
|
+
label?: string;
|
|
23
|
+
emptyText?: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function findMentionQuery(text: string, caret: number): MentionQuery | null;
|
|
26
|
+
export declare function filterMentionCandidates(candidates: MentionCandidate[], query: string): MentionCandidate[];
|
|
27
|
+
/** Rebase untouched mentions; editing any part of a mention removes its identity. */
|
|
28
|
+
export declare function rebaseMentions(previous: string, next: string, mentions: ChatMention[]): ChatMention[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function findMentionQuery(text, caret) {
|
|
2
|
+
const before = text.slice(0, caret);
|
|
3
|
+
// Require a word boundary so email addresses and URLs remain ordinary text.
|
|
4
|
+
const match = /(?:^|[\s([{,。!?、:;])@([^\s@]{0,64})$/u.exec(before);
|
|
5
|
+
if (!match)
|
|
6
|
+
return null;
|
|
7
|
+
return { start: caret - match[1].length - 1, end: caret, query: match[1] };
|
|
8
|
+
}
|
|
9
|
+
export function filterMentionCandidates(candidates, query) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
return candidates.filter(candidate => {
|
|
12
|
+
if (seen.has(candidate.id))
|
|
13
|
+
return false;
|
|
14
|
+
seen.add(candidate.id);
|
|
15
|
+
return candidate.name.toLocaleLowerCase().includes(query.toLocaleLowerCase());
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Rebase untouched mentions; editing any part of a mention removes its identity. */
|
|
19
|
+
export function rebaseMentions(previous, next, mentions) {
|
|
20
|
+
if (previous === next)
|
|
21
|
+
return mentions;
|
|
22
|
+
let start = 0;
|
|
23
|
+
while (start < previous.length && start < next.length && previous[start] === next[start])
|
|
24
|
+
start++;
|
|
25
|
+
let oldEnd = previous.length, newEnd = next.length;
|
|
26
|
+
while (oldEnd > start && newEnd > start && previous[oldEnd - 1] === next[newEnd - 1]) {
|
|
27
|
+
oldEnd--;
|
|
28
|
+
newEnd--;
|
|
29
|
+
}
|
|
30
|
+
const delta = newEnd - oldEnd;
|
|
31
|
+
return mentions.flatMap(mention => {
|
|
32
|
+
if (mention.end <= start)
|
|
33
|
+
return [mention];
|
|
34
|
+
if (mention.start >= oldEnd)
|
|
35
|
+
return [{ ...mention, start: mention.start + delta, end: mention.end + delta }];
|
|
36
|
+
return [];
|
|
37
|
+
}).filter(mention => next.slice(mention.start, mention.end) === `@${mention.name}`
|
|
38
|
+
&& (mention.start === 0 || /[\s([{,。!?、:;]/u.test(next[mention.start - 1]))
|
|
39
|
+
&& (mention.end === next.length || !/[\p{L}\p{N}_@]/u.test(next[mention.end])));
|
|
40
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ChatRuntimeMessage } from "./protocol.js";
|
|
2
|
+
export type ChatActivityState = "thinking" | "searching" | "reading" | "typing" | "idle";
|
|
3
|
+
export type ChatActivityBody = {
|
|
4
|
+
kind: "activity";
|
|
5
|
+
appId: string;
|
|
6
|
+
threadKind: "dm" | "group";
|
|
7
|
+
activityId: string;
|
|
8
|
+
seq: number;
|
|
9
|
+
state: ChatActivityState;
|
|
10
|
+
ttlMs: number;
|
|
11
|
+
/** Short public operation label, never private model reasoning. */
|
|
12
|
+
detail?: string;
|
|
13
|
+
};
|
|
14
|
+
export type ChatActivity = ChatActivityBody & {
|
|
15
|
+
from: string;
|
|
16
|
+
to: string;
|
|
17
|
+
threadId: string;
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
};
|
|
21
|
+
export type ActivityTarget = {
|
|
22
|
+
to: string;
|
|
23
|
+
threadKind?: "dm" | "group";
|
|
24
|
+
recipients?: string[];
|
|
25
|
+
};
|
|
26
|
+
export declare const ACTIVITY_TTL_MS = 15000;
|
|
27
|
+
export declare function createActivityMessage(input: ActivityTarget & {
|
|
28
|
+
from: string;
|
|
29
|
+
appId: string;
|
|
30
|
+
activityId: string;
|
|
31
|
+
seq: number;
|
|
32
|
+
state: ChatActivityState;
|
|
33
|
+
detail?: string;
|
|
34
|
+
}): ChatRuntimeMessage;
|
|
35
|
+
export declare function parseChatActivity(message: ChatRuntimeMessage, appId: string, profileId: string, now?: number): ChatActivity | null;
|
|
36
|
+
/** Keeps ordering tombstones after completion so a late heartbeat cannot revive an activity. */
|
|
37
|
+
export declare class ChatActivityStore {
|
|
38
|
+
private records;
|
|
39
|
+
private listeners;
|
|
40
|
+
private timer;
|
|
41
|
+
watch(listener: (activities: ChatActivity[]) => void): () => void;
|
|
42
|
+
snapshot(now?: number): ChatActivity[];
|
|
43
|
+
accept(activity: ChatActivity): void;
|
|
44
|
+
completeMessage(message: ChatRuntimeMessage, appId: string): void;
|
|
45
|
+
clear(): void;
|
|
46
|
+
private emit;
|
|
47
|
+
}
|
|
48
|
+
/** Coalesced, serialized, best-effort progress; transport failures never reject the chat task. */
|
|
49
|
+
export declare class ChatActivityPublisher {
|
|
50
|
+
private readonly send;
|
|
51
|
+
private readonly onError;
|
|
52
|
+
private seq;
|
|
53
|
+
private stopped;
|
|
54
|
+
private state;
|
|
55
|
+
private detail;
|
|
56
|
+
private pending;
|
|
57
|
+
private sending;
|
|
58
|
+
private timer;
|
|
59
|
+
constructor(send: (value: {
|
|
60
|
+
state: ChatActivityState;
|
|
61
|
+
detail?: string;
|
|
62
|
+
seq: number;
|
|
63
|
+
}) => Promise<unknown>, onError?: (error: unknown) => void);
|
|
64
|
+
update(state: Exclude<ChatActivityState, "idle">, detail?: string): void;
|
|
65
|
+
stop(): Promise<void>;
|
|
66
|
+
private enqueue;
|
|
67
|
+
private flush;
|
|
68
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ChatMsgOp, createRuntimeMsg } from "./protocol.js";
|
|
2
|
+
import { dmThreadId, groupThreadId } from "./thread.js";
|
|
3
|
+
export const ACTIVITY_TTL_MS = 15_000;
|
|
4
|
+
export function createActivityMessage(input) {
|
|
5
|
+
const body = { kind: "activity", appId: input.appId, threadKind: input.threadKind ?? "dm", activityId: input.activityId, seq: input.seq, state: input.state, ttlMs: ACTIVITY_TTL_MS,
|
|
6
|
+
...(input.detail ? { detail: Array.from(input.detail).slice(0, 120).join("") } : {}) };
|
|
7
|
+
const message = createRuntimeMsg(input.from, input.to, ChatMsgOp.CHAT, body);
|
|
8
|
+
if (body.threadKind === "group")
|
|
9
|
+
message.recipients = input.recipients ?? [];
|
|
10
|
+
return message;
|
|
11
|
+
}
|
|
12
|
+
export function parseChatActivity(message, appId, profileId, now = Date.now()) {
|
|
13
|
+
const body = message.body;
|
|
14
|
+
if (message.opcode !== ChatMsgOp.CHAT || body?.kind !== "activity" || body.appId !== appId || !message.from || message.from === profileId || !message.to
|
|
15
|
+
|| (body.threadKind !== "dm" && body.threadKind !== "group")
|
|
16
|
+
|| (body.threadKind === "dm" ? message.to !== profileId : !message.recipients?.includes(profileId))
|
|
17
|
+
|| typeof body.activityId !== "string" || !body.activityId || body.activityId.length > 128
|
|
18
|
+
|| !Number.isSafeInteger(body.seq) || body.seq < 0
|
|
19
|
+
|| !["thinking", "searching", "reading", "typing", "idle"].includes(body.state ?? "")
|
|
20
|
+
|| !Number.isFinite(body.ttlMs) || body.ttlMs < 1000 || body.ttlMs > ACTIVITY_TTL_MS
|
|
21
|
+
|| !Number.isFinite(message.t) || message.t > now + ACTIVITY_TTL_MS || message.t + body.ttlMs <= now
|
|
22
|
+
|| (body.detail !== undefined && (typeof body.detail !== "string" || Array.from(body.detail).length > 120)))
|
|
23
|
+
return null;
|
|
24
|
+
return { ...body, from: message.from, to: message.to,
|
|
25
|
+
threadId: body.threadKind === "dm" ? dmThreadId(appId, message.from, profileId) : groupThreadId(appId, message.to),
|
|
26
|
+
updatedAt: message.t, expiresAt: Math.min(message.t + body.ttlMs, now + ACTIVITY_TTL_MS) };
|
|
27
|
+
}
|
|
28
|
+
/** Keeps ordering tombstones after completion so a late heartbeat cannot revive an activity. */
|
|
29
|
+
export class ChatActivityStore {
|
|
30
|
+
records = new Map();
|
|
31
|
+
listeners = new Set();
|
|
32
|
+
timer;
|
|
33
|
+
watch(listener) { this.listeners.add(listener); listener(this.snapshot()); return () => { this.listeners.delete(listener); }; }
|
|
34
|
+
snapshot(now = Date.now()) { return [...this.records.values()].filter(a => a.state !== "idle" && a.expiresAt > now); }
|
|
35
|
+
accept(activity) {
|
|
36
|
+
const key = JSON.stringify([activity.threadId, activity.from, activity.activityId]);
|
|
37
|
+
const previous = this.records.get(key);
|
|
38
|
+
if (previous && (previous.state === "idle" || previous.seq >= activity.seq))
|
|
39
|
+
return;
|
|
40
|
+
this.records.set(key, activity);
|
|
41
|
+
// Bound memory for long-running clients. Expired tombstones are retained for five minutes.
|
|
42
|
+
for (const [id, record] of this.records)
|
|
43
|
+
if (record.expiresAt + 300_000 < Date.now())
|
|
44
|
+
this.records.delete(id);
|
|
45
|
+
while (this.records.size > 1000)
|
|
46
|
+
this.records.delete(this.records.keys().next().value);
|
|
47
|
+
this.emit();
|
|
48
|
+
}
|
|
49
|
+
completeMessage(message, appId) {
|
|
50
|
+
const body = message.body;
|
|
51
|
+
if (body?.kind !== "message" || body.content?.appId !== appId)
|
|
52
|
+
return;
|
|
53
|
+
let changed = false;
|
|
54
|
+
for (const [key, record] of this.records) {
|
|
55
|
+
if (record.from === message.from && record.to === message.to && record.threadKind === body.threadKind && record.updatedAt <= message.t && record.state !== "idle") {
|
|
56
|
+
this.records.set(key, { ...record, state: "idle" });
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (changed)
|
|
61
|
+
this.emit();
|
|
62
|
+
}
|
|
63
|
+
clear() { this.records.clear(); this.emit(); }
|
|
64
|
+
emit() {
|
|
65
|
+
clearTimeout(this.timer);
|
|
66
|
+
const active = this.snapshot();
|
|
67
|
+
for (const listener of this.listeners)
|
|
68
|
+
listener(active);
|
|
69
|
+
if (active.length)
|
|
70
|
+
this.timer = setTimeout(() => this.emit(), Math.max(1, Math.min(...active.map(a => a.expiresAt)) - Date.now()));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Coalesced, serialized, best-effort progress; transport failures never reject the chat task. */
|
|
74
|
+
export class ChatActivityPublisher {
|
|
75
|
+
send;
|
|
76
|
+
onError;
|
|
77
|
+
seq = 0;
|
|
78
|
+
stopped = false;
|
|
79
|
+
state = "thinking";
|
|
80
|
+
detail;
|
|
81
|
+
pending;
|
|
82
|
+
sending;
|
|
83
|
+
timer;
|
|
84
|
+
constructor(send, onError = () => { }) {
|
|
85
|
+
this.send = send;
|
|
86
|
+
this.onError = onError;
|
|
87
|
+
this.timer = setInterval(() => this.enqueue(this.state, this.detail), 5000);
|
|
88
|
+
}
|
|
89
|
+
update(state, detail) {
|
|
90
|
+
if (this.stopped)
|
|
91
|
+
return;
|
|
92
|
+
this.state = state;
|
|
93
|
+
this.detail = detail;
|
|
94
|
+
this.enqueue(state, detail);
|
|
95
|
+
}
|
|
96
|
+
async stop() {
|
|
97
|
+
if (!this.stopped) {
|
|
98
|
+
this.stopped = true;
|
|
99
|
+
clearInterval(this.timer);
|
|
100
|
+
this.enqueue("idle");
|
|
101
|
+
}
|
|
102
|
+
await this.sending;
|
|
103
|
+
}
|
|
104
|
+
enqueue(state, detail) {
|
|
105
|
+
this.pending = { state, detail };
|
|
106
|
+
if (!this.sending)
|
|
107
|
+
this.sending = this.flush().finally(() => { this.sending = undefined; });
|
|
108
|
+
}
|
|
109
|
+
async flush() {
|
|
110
|
+
while (this.pending) {
|
|
111
|
+
const current = this.pending;
|
|
112
|
+
this.pending = undefined;
|
|
113
|
+
try {
|
|
114
|
+
await this.send({ ...current, seq: this.seq++ });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.onError(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ChatActivityPublisher, type ActivityTarget, type ChatActivity, type ChatActivityState } from "./activity.js";
|
|
2
|
+
import type { ChatMention } from "../mentions/model";
|
|
1
3
|
import { type BotLoginSession } from "./botAuth.js";
|
|
2
4
|
import { type PmateBotKeyFile } from "./botKey.js";
|
|
3
5
|
import { type ChatBody, type ChatRuntimeMessage, type RealtimeClient } from "./protocol.js";
|
|
@@ -10,6 +12,7 @@ export type ChatClientOptions = {
|
|
|
10
12
|
authBaseUrl?: string;
|
|
11
13
|
realtimeClient?: RealtimeClient;
|
|
12
14
|
fetchImpl?: typeof fetch;
|
|
15
|
+
onActivityError?: (error: unknown) => void;
|
|
13
16
|
ackTimeoutMs?: number;
|
|
14
17
|
};
|
|
15
18
|
export declare class ChatClient {
|
|
@@ -18,12 +21,22 @@ export declare class ChatClient {
|
|
|
18
21
|
private session;
|
|
19
22
|
private profileIdValue;
|
|
20
23
|
private botKey;
|
|
24
|
+
private readonly activities;
|
|
25
|
+
private readonly publishers;
|
|
21
26
|
private readonly pendingAcks;
|
|
22
27
|
private readonly messageHandlers;
|
|
23
28
|
constructor(options: ChatClientOptions);
|
|
24
29
|
connect(): Promise<this>;
|
|
25
30
|
close(): void;
|
|
26
31
|
watchMessages(handler: (message: ChatRuntimeMessage) => void): () => boolean;
|
|
32
|
+
watchActivities(handler: (activities: ChatActivity[]) => void): () => void;
|
|
33
|
+
sendActivity(input: ActivityTarget & {
|
|
34
|
+
activityId: string;
|
|
35
|
+
seq: number;
|
|
36
|
+
state: ChatActivityState;
|
|
37
|
+
detail?: string;
|
|
38
|
+
}): Promise<ChatRuntimeMessage>;
|
|
39
|
+
startActivity(target: ActivityTarget, state?: Exclude<ChatActivityState, "idle">): ChatActivityPublisher;
|
|
27
40
|
requestFriend(input: {
|
|
28
41
|
to: string;
|
|
29
42
|
message?: string;
|
|
@@ -55,6 +68,7 @@ export declare class ChatClient {
|
|
|
55
68
|
groupId: string;
|
|
56
69
|
text: string;
|
|
57
70
|
recipients?: string[];
|
|
71
|
+
mentions?: ChatMention[];
|
|
58
72
|
}): Promise<ChatRuntimeMessage>;
|
|
59
73
|
sendChat(to: string, body: ChatBody, recipients?: string[]): Promise<ChatRuntimeMessage>;
|
|
60
74
|
private sendContactAction;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChatActivityStore, ChatActivityPublisher, createActivityMessage, parseChatActivity } from "./activity.js";
|
|
1
2
|
import { loginPmateBot } from "./botAuth.js";
|
|
2
3
|
import { parsePmateBotKeyJson } from "./botKey.js";
|
|
3
4
|
import { ChatMsgKind, ChatMsgOp, ChatRealtimeEvents, RuntimeWebsocketClient, createRuntimeMsg, } from "./protocol.js";
|
|
@@ -10,6 +11,8 @@ export class ChatClient {
|
|
|
10
11
|
session = null;
|
|
11
12
|
profileIdValue = null;
|
|
12
13
|
botKey = null;
|
|
14
|
+
activities = new ChatActivityStore();
|
|
15
|
+
publishers = new Set();
|
|
13
16
|
pendingAcks = new Map();
|
|
14
17
|
messageHandlers = new Set();
|
|
15
18
|
constructor(options) {
|
|
@@ -39,11 +42,17 @@ export class ChatClient {
|
|
|
39
42
|
this.client.on(ChatRealtimeEvents.Message, (message) => {
|
|
40
43
|
this.handleIncoming(message);
|
|
41
44
|
});
|
|
45
|
+
this.client.on(ChatRealtimeEvents.Connected, (connected) => { if (!connected)
|
|
46
|
+
this.activities.clear(); });
|
|
42
47
|
await this.waitUntilConnected();
|
|
43
48
|
await this.sendAuth();
|
|
44
49
|
return this;
|
|
45
50
|
}
|
|
46
51
|
close() {
|
|
52
|
+
for (const publisher of this.publishers)
|
|
53
|
+
void publisher.stop();
|
|
54
|
+
this.publishers.clear();
|
|
55
|
+
this.activities.clear();
|
|
47
56
|
this.client?.close();
|
|
48
57
|
this.client = null;
|
|
49
58
|
for (const pending of this.pendingAcks.values()) {
|
|
@@ -56,6 +65,21 @@ export class ChatClient {
|
|
|
56
65
|
this.messageHandlers.add(handler);
|
|
57
66
|
return () => this.messageHandlers.delete(handler);
|
|
58
67
|
}
|
|
68
|
+
watchActivities(handler) { return this.activities.watch(handler); }
|
|
69
|
+
async sendActivity(input) {
|
|
70
|
+
const message = createActivityMessage({ ...input, from: this.profileId(), appId: this.options.appId });
|
|
71
|
+
await this.sendAndWaitForAck(message);
|
|
72
|
+
return message;
|
|
73
|
+
}
|
|
74
|
+
startActivity(target, state = "typing") {
|
|
75
|
+
const activityId = createRuntimeMsg(this.profileId(), target.to, ChatMsgOp.CHAT, null).hash;
|
|
76
|
+
const publisher = new ChatActivityPublisher(value => this.sendActivity({ ...target, activityId, ...value }), error => this.options.onActivityError?.(error));
|
|
77
|
+
this.publishers.add(publisher);
|
|
78
|
+
const stop = publisher.stop.bind(publisher);
|
|
79
|
+
publisher.stop = async () => { await stop(); this.publishers.delete(publisher); };
|
|
80
|
+
publisher.update(state);
|
|
81
|
+
return publisher;
|
|
82
|
+
}
|
|
59
83
|
async requestFriend(input) {
|
|
60
84
|
return this.sendContactAction(input.to, "friendRequest", {
|
|
61
85
|
message: input.message,
|
|
@@ -104,7 +128,7 @@ export class ChatClient {
|
|
|
104
128
|
kind: "message",
|
|
105
129
|
threadKind: "group",
|
|
106
130
|
contentKind: "text",
|
|
107
|
-
content: { appId: this.options.appId, groupId: input.groupId, text: input.text },
|
|
131
|
+
content: { appId: this.options.appId, groupId: input.groupId, text: input.text, ...(input.mentions?.length ? { mentions: input.mentions } : {}) },
|
|
108
132
|
}, input.recipients);
|
|
109
133
|
}
|
|
110
134
|
async sendChat(to, body, recipients) {
|
|
@@ -145,7 +169,17 @@ export class ChatClient {
|
|
|
145
169
|
}, this.options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS);
|
|
146
170
|
this.pendingAcks.set(msg.hash, { resolve, reject, timer });
|
|
147
171
|
});
|
|
148
|
-
|
|
172
|
+
try {
|
|
173
|
+
await client.sendMsg(msg);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const pending = this.pendingAcks.get(msg.hash);
|
|
177
|
+
if (pending) {
|
|
178
|
+
clearTimeout(pending.timer);
|
|
179
|
+
this.pendingAcks.delete(msg.hash);
|
|
180
|
+
pending.reject(error instanceof Error ? error : new Error("Send failed"));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
149
183
|
return ack;
|
|
150
184
|
}
|
|
151
185
|
handleIncoming(message) {
|
|
@@ -165,6 +199,13 @@ export class ChatClient {
|
|
|
165
199
|
return;
|
|
166
200
|
}
|
|
167
201
|
if (message.opcode === ChatMsgOp.CHAT) {
|
|
202
|
+
if (message.body?.kind === "activity") {
|
|
203
|
+
const activity = parseChatActivity(message, this.options.appId, this.profileId());
|
|
204
|
+
if (activity)
|
|
205
|
+
this.activities.accept(activity);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
this.activities.completeMessage(message, this.options.appId);
|
|
168
209
|
for (const handler of this.messageHandlers) {
|
|
169
210
|
handler(message);
|
|
170
211
|
}
|
package/lib/runtime/index.d.ts
CHANGED
package/lib/runtime/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatActivityBody } from "./activity.js";
|
|
1
2
|
export declare const ChatMsgOp: {
|
|
2
3
|
readonly CHAT: 1;
|
|
3
4
|
readonly ACK: 3;
|
|
@@ -12,7 +13,7 @@ export declare const ChatRealtimeEvents: {
|
|
|
12
13
|
readonly Error: "error";
|
|
13
14
|
readonly Message: "message";
|
|
14
15
|
};
|
|
15
|
-
export type ChatBody = {
|
|
16
|
+
export type ChatBody = ChatActivityBody | {
|
|
16
17
|
kind: "message";
|
|
17
18
|
threadKind: "dm" | "group";
|
|
18
19
|
contentKind: "text" | "image" | "emoji";
|
package/lib/types/chatInput.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MentionConfig, ChatMention } from "../mentions/model";
|
|
1
2
|
import type { ClipboardEventHandler, ComponentType, DragEventHandler, ReactNode } from "react";
|
|
2
3
|
import type { ChatAttachment } from "./attachment";
|
|
3
4
|
import type { ChatVoiceConfig } from "./voice";
|
|
@@ -120,6 +121,8 @@ export type ChatImageConfig = {
|
|
|
120
121
|
uploadImage?: (file: File) => Promise<ChatImageUploadResult>;
|
|
121
122
|
};
|
|
122
123
|
export type ChatInputBaseProps = {
|
|
124
|
+
/** Group members supplied by the host; omit for ordinary text input. */
|
|
125
|
+
mentions?: MentionConfig;
|
|
123
126
|
/** Use a taller composer with actions in a bottom toolbar. */
|
|
124
127
|
multiline?: boolean;
|
|
125
128
|
value?: string;
|
|
@@ -131,6 +134,7 @@ export type ChatInputBaseProps = {
|
|
|
131
134
|
onDrop?: DragEventHandler<HTMLTextAreaElement>;
|
|
132
135
|
onSend?: (payload: {
|
|
133
136
|
text: string;
|
|
137
|
+
mentions?: ChatMention[];
|
|
134
138
|
attachments: ChatAttachment[];
|
|
135
139
|
transcript?: string;
|
|
136
140
|
audio?: Blob;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmate/chat-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -29,15 +29,6 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"clean": "rimraf lib",
|
|
34
|
-
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
|
|
35
|
-
"prepack": "pnpm run build",
|
|
36
|
-
"npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"storybook": "storybook dev -p 6009",
|
|
39
|
-
"build-storybook": "storybook build --output-dir dist"
|
|
40
|
-
},
|
|
41
32
|
"dependencies": {
|
|
42
33
|
"@noble/hashes": "^2.4.0",
|
|
43
34
|
"@emoji-mart/data": "^1.2.1",
|
|
@@ -52,15 +43,23 @@
|
|
|
52
43
|
"devDependencies": {
|
|
53
44
|
"@storybook/addon-docs": "^9.0.17",
|
|
54
45
|
"@storybook/react-vite": "9.0.17",
|
|
55
|
-
"@tailwindcss/vite": "
|
|
56
|
-
"@types/react": "
|
|
57
|
-
"@types/react-dom": "
|
|
58
|
-
"react": "
|
|
59
|
-
"react-dom": "
|
|
46
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
47
|
+
"@types/react": "^19.2.7",
|
|
48
|
+
"@types/react-dom": "^19.2.3",
|
|
49
|
+
"react": "^19.1.1",
|
|
50
|
+
"react-dom": "^19.1.1",
|
|
60
51
|
"rimraf": "^6.1.2",
|
|
61
52
|
"storybook": "^9.0.17",
|
|
62
|
-
"tailwindcss": "
|
|
53
|
+
"tailwindcss": "^4.0.14",
|
|
63
54
|
"typescript": "5.9.2",
|
|
64
55
|
"vitest": "^4.0.17"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"clean": "rimraf lib",
|
|
59
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
|
|
60
|
+
"npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"storybook": "storybook dev -p 6009",
|
|
63
|
+
"build-storybook": "storybook build --output-dir dist"
|
|
65
64
|
}
|
|
66
|
-
}
|
|
65
|
+
}
|