@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 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 = mergeMessages(messages.value, [message]);
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
- if (!disposed && activeGeneration === generation) messages.value = mergeMessages(messages.value, [message]);
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
- if (!disposed) error.value = cause;
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 text = draft().trim();
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 !== false) setDraft("");
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);