@convokitapp/vue-ui 0.2.0 → 0.2.2

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/dist/index.js CHANGED
@@ -31,6 +31,10 @@ import { defineComponent, h } from "vue";
31
31
 
32
32
  // src/utils.ts
33
33
  import { clsx } from "clsx";
34
+ var pendingMessageIdPrefix = "convokit-pending-";
35
+ function isConvoKitPendingMessage(message) {
36
+ return message.id.startsWith(pendingMessageIdPrefix);
37
+ }
34
38
  function cx(...values) {
35
39
  return clsx(values);
36
40
  }
@@ -73,11 +77,15 @@ function mergeMessages(current, incoming) {
73
77
  const byId = new Map(current.map((message) => [message.id, message]));
74
78
  for (const message of incoming) byId.set(message.id, message);
75
79
  return [...byId.values()].sort((left, right) => {
80
+ const leftPending = isConvoKitPendingMessage(left);
81
+ const rightPending = isConvoKitPendingMessage(right);
82
+ if (leftPending !== rightPending) return leftPending ? 1 : -1;
76
83
  const byTime = left.createdAt.getTime() - right.createdAt.getTime();
77
84
  return byTime === 0 ? left.id.localeCompare(right.id) : byTime;
78
85
  });
79
86
  }
80
87
  function readerIdsFor(message, readAtByUserId) {
88
+ if (isConvoKitPendingMessage(message)) return /* @__PURE__ */ new Set();
81
89
  return new Set([...readAtByUserId.entries()].filter(([userId, readAt]) => userId !== message.senderId && readAt.getTime() >= message.createdAt.getTime()).map(([userId]) => userId));
82
90
  }
83
91
  function partClass(part, appearance, defaultClass) {
@@ -165,6 +173,8 @@ function useConversation(options) {
165
173
  let sentTyping = false;
166
174
  let typingTimer = null;
167
175
  let subscriptions = [];
176
+ const pendingIds = /* @__PURE__ */ new Set();
177
+ let pendingSequence = 0;
168
178
  const unsubscribe = async () => {
169
179
  const active = subscriptions;
170
180
  subscriptions = [];
@@ -188,7 +198,12 @@ function useConversation(options) {
188
198
  subscriptions = [
189
199
  client.onMessage(conversationId, (message) => {
190
200
  if (disposed || activeGeneration !== generation) return;
191
- messages.value = mergeMessages(messages.value, [message]);
201
+ const pending = messages.value.find((candidate) => pendingIds.has(candidate.id) && message.senderId === client.currentUserId && candidate.text === message.text && candidate.media.length === message.media.length);
202
+ if (pending) pendingIds.delete(pending.id);
203
+ messages.value = mergeMessages(
204
+ pending ? messages.value.filter((candidate) => candidate.id !== pending.id) : messages.value,
205
+ [message]
206
+ );
192
207
  if ((options.markReadOnReceive ?? true) && message.senderId !== client.currentUserId) void markRead();
193
208
  }, report),
194
209
  client.onReadReceipt(conversationId, ({ userId, readAt }) => {
@@ -212,6 +227,7 @@ function useConversation(options) {
212
227
  await unsubscribe();
213
228
  if (disposed || activeGeneration !== generation) return;
214
229
  messages.value = [];
230
+ pendingIds.clear();
215
231
  conversation.value = null;
216
232
  typingUserIds.value = /* @__PURE__ */ new Set();
217
233
  readAtByUserId.value = /* @__PURE__ */ new Map();
@@ -251,7 +267,7 @@ function useConversation(options) {
251
267
  const page = await client.getMessages({
252
268
  conversationId,
253
269
  limit: messagePageSize,
254
- offset: messages.value.length
270
+ offset: messages.value.filter((message) => !pendingIds.has(message.id)).length
255
271
  });
256
272
  if (disposed || activeGeneration !== generation) return;
257
273
  messages.value = mergeMessages(messages.value, page);
@@ -282,6 +298,18 @@ function useConversation(options) {
282
298
  const normalizedText = text?.trim();
283
299
  if (!normalizedText && (!media || media.length === 0)) return null;
284
300
  if (isSending.value) return null;
301
+ const pendingId = `convokit-pending-${Date.now()}-${++pendingSequence}`;
302
+ const pendingMessage = {
303
+ id: pendingId,
304
+ conversationId: toValue(options.conversationId),
305
+ senderId: toValue(options.client).currentUserId,
306
+ text: normalizedText ?? null,
307
+ media: media ?? [],
308
+ createdAt: /* @__PURE__ */ new Date(),
309
+ updatedAt: null
310
+ };
311
+ pendingIds.add(pendingId);
312
+ messages.value = mergeMessages(messages.value, [pendingMessage]);
285
313
  isSending.value = true;
286
314
  error.value = null;
287
315
  const activeGeneration = generation;
@@ -291,11 +319,21 @@ function useConversation(options) {
291
319
  ...normalizedText ? { text: normalizedText } : {},
292
320
  ...media?.length ? { media } : {}
293
321
  });
294
- if (!disposed && activeGeneration === generation) messages.value = mergeMessages(messages.value, [message]);
322
+ pendingIds.delete(pendingId);
323
+ if (!disposed && activeGeneration === generation) {
324
+ messages.value = mergeMessages(
325
+ messages.value.filter((candidate) => candidate.id !== pendingId),
326
+ [message]
327
+ );
328
+ }
295
329
  await updateTyping(false);
296
330
  return message;
297
331
  } catch (cause) {
298
- if (!disposed) error.value = cause;
332
+ pendingIds.delete(pendingId);
333
+ if (!disposed) {
334
+ messages.value = messages.value.filter((candidate) => candidate.id !== pendingId);
335
+ error.value = cause;
336
+ }
299
337
  return null;
300
338
  } finally {
301
339
  if (!disposed && activeGeneration === generation) isSending.value = false;
@@ -476,7 +514,8 @@ var MessageListView = defineComponent2({
476
514
  const renderMessage = (message, index) => {
477
515
  const isCurrentUser = message.senderId === props.currentUserId;
478
516
  const sender = participants.value.get(message.senderId);
479
- const readerIds = props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId);
517
+ const isPending = isConvoKitPendingMessage(message);
518
+ const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId);
480
519
  const slotProps = { message, chronologicalIndex: index, isCurrentUser, sender, readerIds };
481
520
  const custom = slots.message?.(slotProps);
482
521
  if (custom) return h2("div", { key: message.id, role: "listitem" }, custom);
@@ -509,12 +548,12 @@ var MessageListView = defineComponent2({
509
548
  !isCurrentUser ? h2("strong", { class: "ckui-message-sender" }, sender?.name || message.senderId) : null,
510
549
  message.text ? h2("div", { class: "ckui-message-text" }, message.text) : null,
511
550
  ...mediaNodes,
512
- h2("time", { class: "ckui-message-time", datetime: message.createdAt.toISOString() }, [
513
- props.formatTime(message.createdAt),
514
- isCurrentUser ? readerIds.size > 0 ? h2(CheckCheck, { size: 14, "aria-label": "Read" }) : h2(Check, { size: 14, "aria-label": "Delivered" }) : null
551
+ h2("span", { class: "ckui-message-time" }, [
552
+ isPending ? "Sending\u2026" : props.formatTime(message.createdAt),
553
+ isCurrentUser && !isPending ? readerIds.size > 0 ? h2(CheckCheck, { size: 14, "aria-label": "Read" }) : h2(Check, { size: 14, "aria-label": "Delivered" }) : null
515
554
  ])
516
555
  ]),
517
- isCurrentUser ? slots["read-receipt"]?.(receiptSlotProps) ?? h2("div", {
556
+ isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? h2("div", {
518
557
  class: partClass("receipt", currentAppearance, "ckui-read-receipt"),
519
558
  style: partStyle("receipt", currentAppearance)
520
559
  }, readerIds.size > 0 ? `Read by ${readerIds.size}` : "Delivered") : null
@@ -651,7 +690,9 @@ var ConversationView = defineComponent3({
651
690
  ...props.styles ? { styles: props.styles } : {}
652
691
  });
653
692
  const draft = () => props.modelValue ?? internalDraft.value;
693
+ let latestDraft = draft();
654
694
  const setDraft = (value) => {
695
+ latestDraft = value;
655
696
  if (props.modelValue === void 0) internalDraft.value = value;
656
697
  props.onDraftChange?.(value);
657
698
  emit("update:modelValue", value);
@@ -659,12 +700,14 @@ var ConversationView = defineComponent3({
659
700
  void props.onTypingChange?.(isTyping);
660
701
  };
661
702
  const submit = async () => {
662
- const text = draft().trim();
703
+ const originalDraft = draft();
704
+ const text = originalDraft.trim();
663
705
  if (!text || props.isSending || submitting.value) return;
664
706
  submitting.value = true;
707
+ setDraft("");
665
708
  try {
666
709
  const shouldClear = await props.onSendMessage(text);
667
- if (shouldClear !== false) setDraft("");
710
+ if (shouldClear === false && latestDraft.length === 0) setDraft(originalDraft);
668
711
  } finally {
669
712
  submitting.value = false;
670
713
  }
@@ -757,7 +800,6 @@ var ConversationView = defineComponent3({
757
800
  class: cx(!props.unstyled && "ckui-composer__input", props.classNames?.input, props.composerProps?.class),
758
801
  style: [props.styles?.input, props.composerProps?.style],
759
802
  value: draft(),
760
- disabled: props.isSending || submitting.value,
761
803
  onInput: (event) => {
762
804
  const handler = props.composerProps?.onInput;
763
805
  if (typeof handler === "function") handler(event);
@@ -1398,6 +1440,7 @@ export {
1398
1440
  defaultConvoKitTheme,
1399
1441
  defaultReadersResolver,
1400
1442
  formatFileSize,
1443
+ isConvoKitPendingMessage,
1401
1444
  matchesConversation,
1402
1445
  mergeConversations,
1403
1446
  mergeMessages,