@convokitapp/vue-ui 0.1.3 → 0.2.1
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/CHANGELOG.md +11 -0
- package/README.md +4 -0
- package/dist/index.cjs +40 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +40 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- Show outgoing messages immediately while the request completes, then reconcile the server response and realtime echo without duplicates.
|
|
6
|
+
- Clear the composer immediately and restore its draft after a failed send when the user has not typed a replacement.
|
|
7
|
+
|
|
8
|
+
## 0.2.0
|
|
9
|
+
|
|
10
|
+
- Require ConvoKit JavaScript SDK 0.2.x for private Realtime channels and publishable-key discovery.
|
|
11
|
+
- Preserve the existing Vue component and configuration API.
|
package/README.md
CHANGED
|
@@ -54,6 +54,10 @@ Vite environment variables, or a shipped browser bundle.
|
|
|
54
54
|
The core SDK uses ConvoKit's managed `https://api.convokit.app` endpoint. Set
|
|
55
55
|
`backendUrl` only for local testing or a self-hosted deployment.
|
|
56
56
|
|
|
57
|
+
SDK-backed conversations render an outgoing message immediately, reconcile it
|
|
58
|
+
with the server response and realtime echo, and restore an unchanged draft if
|
|
59
|
+
the send fails.
|
|
60
|
+
|
|
57
61
|
## Controlled components and slots
|
|
58
62
|
|
|
59
63
|
`ConversationListView`, `MessageListView`, and `ConversationView` accept host
|
package/dist/index.cjs
CHANGED
|
@@ -196,6 +196,8 @@ function useConversation(options) {
|
|
|
196
196
|
let sentTyping = false;
|
|
197
197
|
let typingTimer = null;
|
|
198
198
|
let subscriptions = [];
|
|
199
|
+
const pendingIds = /* @__PURE__ */ new Set();
|
|
200
|
+
let pendingSequence = 0;
|
|
199
201
|
const unsubscribe = async () => {
|
|
200
202
|
const active = subscriptions;
|
|
201
203
|
subscriptions = [];
|
|
@@ -219,7 +221,12 @@ function useConversation(options) {
|
|
|
219
221
|
subscriptions = [
|
|
220
222
|
client.onMessage(conversationId, (message) => {
|
|
221
223
|
if (disposed || activeGeneration !== generation) return;
|
|
222
|
-
messages.value
|
|
224
|
+
const pending = messages.value.find((candidate) => pendingIds.has(candidate.id) && message.senderId === client.currentUserId && candidate.text === message.text && candidate.media.length === message.media.length);
|
|
225
|
+
if (pending) pendingIds.delete(pending.id);
|
|
226
|
+
messages.value = mergeMessages(
|
|
227
|
+
pending ? messages.value.filter((candidate) => candidate.id !== pending.id) : messages.value,
|
|
228
|
+
[message]
|
|
229
|
+
);
|
|
223
230
|
if ((options.markReadOnReceive ?? true) && message.senderId !== client.currentUserId) void markRead();
|
|
224
231
|
}, report),
|
|
225
232
|
client.onReadReceipt(conversationId, ({ userId, readAt }) => {
|
|
@@ -243,6 +250,7 @@ function useConversation(options) {
|
|
|
243
250
|
await unsubscribe();
|
|
244
251
|
if (disposed || activeGeneration !== generation) return;
|
|
245
252
|
messages.value = [];
|
|
253
|
+
pendingIds.clear();
|
|
246
254
|
conversation.value = null;
|
|
247
255
|
typingUserIds.value = /* @__PURE__ */ new Set();
|
|
248
256
|
readAtByUserId.value = /* @__PURE__ */ new Map();
|
|
@@ -282,7 +290,7 @@ function useConversation(options) {
|
|
|
282
290
|
const page = await client.getMessages({
|
|
283
291
|
conversationId,
|
|
284
292
|
limit: messagePageSize,
|
|
285
|
-
offset: messages.value.length
|
|
293
|
+
offset: messages.value.filter((message) => !pendingIds.has(message.id)).length
|
|
286
294
|
});
|
|
287
295
|
if (disposed || activeGeneration !== generation) return;
|
|
288
296
|
messages.value = mergeMessages(messages.value, page);
|
|
@@ -313,6 +321,18 @@ function useConversation(options) {
|
|
|
313
321
|
const normalizedText = text?.trim();
|
|
314
322
|
if (!normalizedText && (!media || media.length === 0)) return null;
|
|
315
323
|
if (isSending.value) return null;
|
|
324
|
+
const pendingId = `convokit-pending-${Date.now()}-${++pendingSequence}`;
|
|
325
|
+
const pendingMessage = {
|
|
326
|
+
id: pendingId,
|
|
327
|
+
conversationId: (0, import_vue2.toValue)(options.conversationId),
|
|
328
|
+
senderId: (0, import_vue2.toValue)(options.client).currentUserId,
|
|
329
|
+
text: normalizedText ?? null,
|
|
330
|
+
media: media ?? [],
|
|
331
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
332
|
+
updatedAt: null
|
|
333
|
+
};
|
|
334
|
+
pendingIds.add(pendingId);
|
|
335
|
+
messages.value = mergeMessages(messages.value, [pendingMessage]);
|
|
316
336
|
isSending.value = true;
|
|
317
337
|
error.value = null;
|
|
318
338
|
const activeGeneration = generation;
|
|
@@ -322,11 +342,21 @@ function useConversation(options) {
|
|
|
322
342
|
...normalizedText ? { text: normalizedText } : {},
|
|
323
343
|
...media?.length ? { media } : {}
|
|
324
344
|
});
|
|
325
|
-
|
|
345
|
+
pendingIds.delete(pendingId);
|
|
346
|
+
if (!disposed && activeGeneration === generation) {
|
|
347
|
+
messages.value = mergeMessages(
|
|
348
|
+
messages.value.filter((candidate) => candidate.id !== pendingId),
|
|
349
|
+
[message]
|
|
350
|
+
);
|
|
351
|
+
}
|
|
326
352
|
await updateTyping(false);
|
|
327
353
|
return message;
|
|
328
354
|
} catch (cause) {
|
|
329
|
-
|
|
355
|
+
pendingIds.delete(pendingId);
|
|
356
|
+
if (!disposed) {
|
|
357
|
+
messages.value = messages.value.filter((candidate) => candidate.id !== pendingId);
|
|
358
|
+
error.value = cause;
|
|
359
|
+
}
|
|
330
360
|
return null;
|
|
331
361
|
} finally {
|
|
332
362
|
if (!disposed && activeGeneration === generation) isSending.value = false;
|
|
@@ -665,7 +695,9 @@ var ConversationView = (0, import_vue6.defineComponent)({
|
|
|
665
695
|
...props.styles ? { styles: props.styles } : {}
|
|
666
696
|
});
|
|
667
697
|
const draft = () => props.modelValue ?? internalDraft.value;
|
|
698
|
+
let latestDraft = draft();
|
|
668
699
|
const setDraft = (value) => {
|
|
700
|
+
latestDraft = value;
|
|
669
701
|
if (props.modelValue === void 0) internalDraft.value = value;
|
|
670
702
|
props.onDraftChange?.(value);
|
|
671
703
|
emit("update:modelValue", value);
|
|
@@ -673,12 +705,14 @@ var ConversationView = (0, import_vue6.defineComponent)({
|
|
|
673
705
|
void props.onTypingChange?.(isTyping);
|
|
674
706
|
};
|
|
675
707
|
const submit = async () => {
|
|
676
|
-
const
|
|
708
|
+
const originalDraft = draft();
|
|
709
|
+
const text = originalDraft.trim();
|
|
677
710
|
if (!text || props.isSending || submitting.value) return;
|
|
678
711
|
submitting.value = true;
|
|
712
|
+
setDraft("");
|
|
679
713
|
try {
|
|
680
714
|
const shouldClear = await props.onSendMessage(text);
|
|
681
|
-
if (shouldClear
|
|
715
|
+
if (shouldClear === false && latestDraft.length === 0) setDraft(originalDraft);
|
|
682
716
|
} finally {
|
|
683
717
|
submitting.value = false;
|
|
684
718
|
}
|
|
@@ -771,7 +805,6 @@ var ConversationView = (0, import_vue6.defineComponent)({
|
|
|
771
805
|
class: cx(!props.unstyled && "ckui-composer__input", props.classNames?.input, props.composerProps?.class),
|
|
772
806
|
style: [props.styles?.input, props.composerProps?.style],
|
|
773
807
|
value: draft(),
|
|
774
|
-
disabled: props.isSending || submitting.value,
|
|
775
808
|
onInput: (event) => {
|
|
776
809
|
const handler = props.composerProps?.onInput;
|
|
777
810
|
if (typeof handler === "function") handler(event);
|