@convokitapp/vue-ui 0.7.0 → 0.8.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/CHANGELOG.md +100 -0
- package/PARITY.md +37 -0
- package/README.md +118 -4
- package/dist/index.cjs +411 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +93 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +265 -5
- package/dist/index.d.ts +265 -5
- package/dist/index.js +418 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -26,6 +26,8 @@ function createConvoKitUiClient(client) {
|
|
|
26
26
|
getMessages: (options) => client.getMessages(options),
|
|
27
27
|
getMessage: (id) => client.getMessage(id),
|
|
28
28
|
sendMessage: (input) => client.sendMessage(input),
|
|
29
|
+
editMessage: (messageId, input) => client.editMessage(messageId, input),
|
|
30
|
+
deleteMessage: (messageId) => client.deleteMessage(messageId),
|
|
29
31
|
markConversationRead: (conversationId, options) => client.markConversationRead(conversationId, options),
|
|
30
32
|
markConversationUnread: (conversationId) => client.markConversationUnread(conversationId),
|
|
31
33
|
clearConversationUnread: (conversationId, options) => client.clearConversationUnread(conversationId, options),
|
|
@@ -186,12 +188,13 @@ var ConvoKitAvatar = defineComponent({
|
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
// src/components/conversation.ts
|
|
189
|
-
import { ArrowLeft, LoaderCircle as LoaderCircle2, Paperclip, RefreshCw, Send } from "@lucide/vue";
|
|
191
|
+
import { ArrowLeft, Check as Check2, LoaderCircle as LoaderCircle2, Paperclip, Pencil as Pencil2, RefreshCw, Send } from "@lucide/vue";
|
|
190
192
|
import {
|
|
191
193
|
defineComponent as defineComponent3,
|
|
192
194
|
h as h3,
|
|
193
195
|
onBeforeUnmount,
|
|
194
196
|
ref as ref2,
|
|
197
|
+
watch as watch3,
|
|
195
198
|
watchEffect
|
|
196
199
|
} from "vue";
|
|
197
200
|
|
|
@@ -206,9 +209,31 @@ function version(message) {
|
|
|
206
209
|
function hasContent(message) {
|
|
207
210
|
return !!message.text?.trim() || message.media.length > 0;
|
|
208
211
|
}
|
|
212
|
+
function revisionOrder(left, right) {
|
|
213
|
+
const a = left.revision, b = right.revision;
|
|
214
|
+
if (typeof a !== "number" || typeof b !== "number" || a <= 0 && b <= 0) return void 0;
|
|
215
|
+
return a === b ? void 0 : a - b;
|
|
216
|
+
}
|
|
217
|
+
function older(candidate, reference) {
|
|
218
|
+
const byRevision = revisionOrder(candidate, reference);
|
|
219
|
+
return byRevision === void 0 ? version(candidate) < version(reference) : byRevision < 0;
|
|
220
|
+
}
|
|
209
221
|
function newest(current, incoming, incomingComplete = true) {
|
|
222
|
+
const byRevision = revisionOrder(current, incoming);
|
|
223
|
+
if (byRevision !== void 0) return byRevision > 0 ? current : incoming;
|
|
210
224
|
return version(current) > version(incoming) || !incomingComplete && version(current) === version(incoming) ? current : incoming;
|
|
211
225
|
}
|
|
226
|
+
function isRevisionConflict(cause) {
|
|
227
|
+
if (typeof cause !== "object" || cause === null) return false;
|
|
228
|
+
const { code, status } = cause;
|
|
229
|
+
return code === "REVISION_CONFLICT" || code === void 0 && status === 409;
|
|
230
|
+
}
|
|
231
|
+
function isMessageMissing(cause) {
|
|
232
|
+
return typeof cause === "object" && cause !== null && cause.code === "MESSAGE_NOT_FOUND";
|
|
233
|
+
}
|
|
234
|
+
function localConflict() {
|
|
235
|
+
return Object.assign(new Error("Message was changed since it was loaded"), { code: "REVISION_CONFLICT" });
|
|
236
|
+
}
|
|
212
237
|
var compare = compareMessageOrder;
|
|
213
238
|
function positionCursor(position) {
|
|
214
239
|
return { createdAt: position.createdAt, id: position.messageId };
|
|
@@ -228,7 +253,7 @@ function isTargetMiss(cause) {
|
|
|
228
253
|
const { code, status } = cause;
|
|
229
254
|
return code === "MESSAGE_NOT_FOUND" || code === void 0 && status === 404;
|
|
230
255
|
}
|
|
231
|
-
function blank(currentUserId = "") {
|
|
256
|
+
function blank(currentUserId = "", support = { edit: false, delete: false }) {
|
|
232
257
|
return {
|
|
233
258
|
conversation: null,
|
|
234
259
|
messages: [],
|
|
@@ -242,7 +267,10 @@ function blank(currentUserId = "") {
|
|
|
242
267
|
hasOlderMessages: true,
|
|
243
268
|
hasLoaded: false,
|
|
244
269
|
error: null,
|
|
245
|
-
currentUserId
|
|
270
|
+
currentUserId,
|
|
271
|
+
editingMessage: null,
|
|
272
|
+
canEditMessages: support.edit,
|
|
273
|
+
canDeleteMessages: support.delete
|
|
246
274
|
};
|
|
247
275
|
}
|
|
248
276
|
var ConversationStore = class {
|
|
@@ -261,7 +289,8 @@ var ConversationStore = class {
|
|
|
261
289
|
}
|
|
262
290
|
this.owner = this.client.sessionIdentity;
|
|
263
291
|
this.user = this.owner ? this.client.currentUserId : "";
|
|
264
|
-
this.
|
|
292
|
+
this.support = { edit: typeof this.client.editMessage === "function", delete: typeof this.client.deleteMessage === "function" };
|
|
293
|
+
this.state = blank(this.user, this.support);
|
|
265
294
|
}
|
|
266
295
|
options;
|
|
267
296
|
client;
|
|
@@ -288,6 +317,13 @@ var ConversationStore = class {
|
|
|
288
317
|
visible = true;
|
|
289
318
|
sendRevision;
|
|
290
319
|
activeSend;
|
|
320
|
+
/** Adapter support for author edits/deletes, decided once like `listInbox`. */
|
|
321
|
+
support;
|
|
322
|
+
/** The id whose `saveEdit` request is in flight: its outcome (success or 409) decides the edit, so newer rows for
|
|
323
|
+
* it arriving meanwhile (its own UPDATE image, typically) are not reported as a local conflict while it lasts;
|
|
324
|
+
* `settleEditing` re-evaluates them once the request has settled any other way.
|
|
325
|
+
*/
|
|
326
|
+
activeEdit;
|
|
291
327
|
refreshQueued = false;
|
|
292
328
|
typingTimers = /* @__PURE__ */ new Map();
|
|
293
329
|
ownTypingTimer;
|
|
@@ -306,9 +342,33 @@ var ConversationStore = class {
|
|
|
306
342
|
for (const message of patch.messages) this.confirmSend(message);
|
|
307
343
|
if (this.activeSend.confirmed) patch.messages = patch.messages.filter((message) => message.id !== this.activeSend.pending.id);
|
|
308
344
|
}
|
|
345
|
+
if (patch.messages) patch = this.trackEditing(patch);
|
|
309
346
|
this.state = { ...this.state, ...patch };
|
|
310
347
|
for (const listener of this.listeners) listener();
|
|
311
348
|
}
|
|
349
|
+
/** Edit mode follows the edited row wherever a message list reaches the state: the row leaving the list (deletion,
|
|
350
|
+
* reconcile tombstone, eviction) ends it, and a row for it with a higher revision than the snapshot (UPDATE image,
|
|
351
|
+
* hydration, reconcile, refresh) is the local conflict: the snapshot is replaced and `error` carries the conflict
|
|
352
|
+
* code, without a request. A save in flight owns its own outcome (`activeEdit`) and re-checks when it settles
|
|
353
|
+
* (`settleEditing`).
|
|
354
|
+
*/
|
|
355
|
+
trackEditing(patch) {
|
|
356
|
+
const editing = patch.editingMessage === void 0 ? this.state.editingMessage : patch.editingMessage;
|
|
357
|
+
if (!editing || !patch.messages) return patch;
|
|
358
|
+
const live = patch.messages.find((message) => message.id === editing.id);
|
|
359
|
+
if (!live) return { ...patch, editingMessage: null };
|
|
360
|
+
if (this.activeEdit === editing.id || !(live.revision > editing.revision)) return patch;
|
|
361
|
+
return { ...patch, editingMessage: live, error: localConflict() };
|
|
362
|
+
}
|
|
363
|
+
/** After a save for `id` has settled without deciding the edit (a failure, or a 409 whose reload failed), a newer
|
|
364
|
+
* row for it that arrived during the request is the local conflict after all: the snapshot is replaced and
|
|
365
|
+
* `error` carries the conflict code, so the next save carries the fresh revision without another round trip.
|
|
366
|
+
*/
|
|
367
|
+
settleEditing(id) {
|
|
368
|
+
const editing = this.state.editingMessage;
|
|
369
|
+
const live = editing?.id === id ? this.state.messages.find((message) => message.id === id) : void 0;
|
|
370
|
+
if (editing && live && live.revision > editing.revision) this.patch({ editingMessage: live, error: localConflict() });
|
|
371
|
+
}
|
|
312
372
|
alive(generation = this.generation) {
|
|
313
373
|
return !this.disposed && generation === this.generation && this.owner !== null && this.client.sessionIdentity === this.owner;
|
|
314
374
|
}
|
|
@@ -352,6 +412,7 @@ var ConversationStore = class {
|
|
|
352
412
|
this.captured = capture();
|
|
353
413
|
this.sendRevision = void 0;
|
|
354
414
|
this.activeSend = void 0;
|
|
415
|
+
this.activeEdit = void 0;
|
|
355
416
|
this.refreshQueued = false;
|
|
356
417
|
}
|
|
357
418
|
dispose = () => {
|
|
@@ -360,14 +421,14 @@ var ConversationStore = class {
|
|
|
360
421
|
}
|
|
361
422
|
this.disposed = true;
|
|
362
423
|
this.clear();
|
|
363
|
-
this.patch(blank());
|
|
424
|
+
this.patch(blank("", this.support));
|
|
364
425
|
};
|
|
365
426
|
fail(cause, generation, history = false) {
|
|
366
427
|
if (!this.alive(generation)) return;
|
|
367
428
|
const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
368
429
|
if (history && (status === 401 || status === 403 || status === 404)) {
|
|
369
430
|
this.clear();
|
|
370
|
-
this.patch({ ...blank(this.user), error: cause, hasLoaded: true, hasOlderMessages: false });
|
|
431
|
+
this.patch({ ...blank(this.user, this.support), error: cause, hasLoaded: true, hasOlderMessages: false });
|
|
371
432
|
} else this.patch({ error: cause });
|
|
372
433
|
}
|
|
373
434
|
attach(generation, data = true) {
|
|
@@ -439,7 +500,7 @@ var ConversationStore = class {
|
|
|
439
500
|
if (!this.alive(generation) || type !== "insert" && type !== "update" || !this.validMessage(message) || this.deleted.has(message.id)) return;
|
|
440
501
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
441
502
|
const known = existing ?? this.changes.get(message.id)?.message;
|
|
442
|
-
if (known &&
|
|
503
|
+
if (known && older(message, known)) return;
|
|
443
504
|
const insert = type === "insert" || this.changes.get(message.id)?.insert === true;
|
|
444
505
|
if (!existing && !insert && type === "update" && !this.state.isInitialLoading && !this.state.isLoadingOlder && !this.state.isReconciling) return;
|
|
445
506
|
const revision = ++this.revision;
|
|
@@ -452,7 +513,7 @@ var ConversationStore = class {
|
|
|
452
513
|
}
|
|
453
514
|
record(message, insert, revision, complete) {
|
|
454
515
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
455
|
-
if (existing &&
|
|
516
|
+
if (existing && older(message, existing)) return;
|
|
456
517
|
if (this.confirmSend(message) && !complete && !message.media.length) {
|
|
457
518
|
message = { ...message, media: this.activeSend.pending.media };
|
|
458
519
|
this.confirmSend(message);
|
|
@@ -503,7 +564,7 @@ var ConversationStore = class {
|
|
|
503
564
|
if (!this.currentHydration(job)) return;
|
|
504
565
|
const full = await this.client.getMessage(id);
|
|
505
566
|
if (!this.currentHydration(job)) return;
|
|
506
|
-
if (!this.validMessage(full) || full.id !== id || full.senderId !== job.message.senderId ||
|
|
567
|
+
if (!this.validMessage(full) || full.id !== id || full.senderId !== job.message.senderId || older(full, job.message)) {
|
|
507
568
|
throw new Error("Complete message response does not match the observed resource/revision");
|
|
508
569
|
}
|
|
509
570
|
this.record(full, job.insert, job.revision, true);
|
|
@@ -583,7 +644,7 @@ var ConversationStore = class {
|
|
|
583
644
|
if (!this.alive()) return;
|
|
584
645
|
this.clear();
|
|
585
646
|
const generation = this.generation;
|
|
586
|
-
this.patch({ ...blank(this.user), isInitialLoading: true });
|
|
647
|
+
this.patch({ ...blank(this.user, this.support), isInitialLoading: true });
|
|
587
648
|
const revision = this.revision;
|
|
588
649
|
try {
|
|
589
650
|
this.attach(generation);
|
|
@@ -826,7 +887,8 @@ var ConversationStore = class {
|
|
|
826
887
|
text: normalized || null,
|
|
827
888
|
media: media ?? [],
|
|
828
889
|
createdAt: /* @__PURE__ */ new Date(),
|
|
829
|
-
updatedAt: null
|
|
890
|
+
updatedAt: null,
|
|
891
|
+
revision: 0
|
|
830
892
|
};
|
|
831
893
|
const send = { pending };
|
|
832
894
|
this.activeSend = send;
|
|
@@ -870,6 +932,132 @@ var ConversationStore = class {
|
|
|
870
932
|
}
|
|
871
933
|
}
|
|
872
934
|
};
|
|
935
|
+
/** The caller's role in the open room when known (0.7 `membership`, else the caller's participant row). */
|
|
936
|
+
ownRole() {
|
|
937
|
+
const conversation = this.state.conversation;
|
|
938
|
+
return conversation?.membership?.role ?? conversation?.participants.find((participant) => participant.appUserId === this.user || participant.id === this.user)?.role;
|
|
939
|
+
}
|
|
940
|
+
/** A rendered, confirmed row of the caller's own that is not known to be gone. */
|
|
941
|
+
ownRow(messageId) {
|
|
942
|
+
const row = this.state.messages.find((message) => message.id === messageId);
|
|
943
|
+
return row && row.senderId === this.user && !isConvoKitPendingMessage(row) && !this.deleted.has(messageId) ? row : void 0;
|
|
944
|
+
}
|
|
945
|
+
/** Enter edit mode on one of the caller's own confirmed messages (0.8.0): the row as it stands now becomes the
|
|
946
|
+
* snapshot whose `revision` every save sends. A no-op unless the adapter implements `editMessage`, the row is
|
|
947
|
+
* rendered, own, confirmed, not tombstoned and the caller's role (when known) is not `READ`. Sends nothing.
|
|
948
|
+
*/
|
|
949
|
+
startEditing = (messageId) => {
|
|
950
|
+
if (!this.alive() || !this.support.edit || this.ownRole() === "READ") return;
|
|
951
|
+
const row = this.ownRow(messageId);
|
|
952
|
+
if (row) this.patch({ editingMessage: row });
|
|
953
|
+
};
|
|
954
|
+
/** Leave edit mode without a request; the draft is the view's to restore. */
|
|
955
|
+
cancelEditing = () => {
|
|
956
|
+
if (this.state.editingMessage) this.patch({ editingMessage: null });
|
|
957
|
+
};
|
|
958
|
+
/** Save the edit in progress with the snapshot's revision (never the live row's), trimming the text and sending
|
|
959
|
+
* `null` for an empty caption. Resolves true when the server accepted the edit (the response is merged through the
|
|
960
|
+
* tombstone and precedence guards and edit mode ends); false when nothing was saved: a stale revision (409
|
|
961
|
+
* `REVISION_CONFLICT`) reloads the row once through `getMessage`, replaces the snapshot with it (the next save
|
|
962
|
+
* carries the fresh revision) and reports the conflict through `error`, keeping edit mode; a coded 404
|
|
963
|
+
* (`MESSAGE_NOT_FOUND`, on the save or on that reload) removes the row and ends edit mode; any other failure
|
|
964
|
+
* (403, 500, network, an uncoded 404 from a 0.7 backend) is reported through `error` without evicting anything and
|
|
965
|
+
* keeps edit mode; if a newer row for the message arrived during such a request, that row is then the local
|
|
966
|
+
* conflict (`settleEditing`). A text-only message cannot be saved empty (no request). Rejects when the adapter
|
|
967
|
+
* lacks `editMessage`.
|
|
968
|
+
*/
|
|
969
|
+
saveEdit = async (text) => {
|
|
970
|
+
const client = this.client;
|
|
971
|
+
if (typeof client.editMessage !== "function") {
|
|
972
|
+
throw new TypeError("This ConvoKitUiClient adapter does not implement editMessage (0.8)");
|
|
973
|
+
}
|
|
974
|
+
const snapshot = this.state.editingMessage;
|
|
975
|
+
if (!this.alive() || !snapshot || this.activeEdit !== void 0) return false;
|
|
976
|
+
const trimmed = text.trim();
|
|
977
|
+
const normalized = trimmed === "" ? null : trimmed;
|
|
978
|
+
if (normalized === null && snapshot.media.length === 0) return false;
|
|
979
|
+
const generation = this.generation;
|
|
980
|
+
const id = snapshot.id;
|
|
981
|
+
this.activeEdit = id;
|
|
982
|
+
this.patch({ error: null });
|
|
983
|
+
try {
|
|
984
|
+
const message = await client.editMessage(id, { text: normalized, revision: snapshot.revision });
|
|
985
|
+
if (!this.alive(generation)) return false;
|
|
986
|
+
if (!this.validMessage(message) || message.id !== id || message.senderId !== this.user) {
|
|
987
|
+
throw new Error("Edit response belongs to a different message or sender");
|
|
988
|
+
}
|
|
989
|
+
if (this.state.editingMessage?.id === id) this.patch({ editingMessage: null });
|
|
990
|
+
this.applyRow(message);
|
|
991
|
+
return true;
|
|
992
|
+
} catch (cause) {
|
|
993
|
+
if (!this.alive(generation)) return false;
|
|
994
|
+
if (isRevisionConflict(cause)) await this.reloadConflict(id, cause, generation);
|
|
995
|
+
else if (isMessageMissing(cause)) {
|
|
996
|
+
this.removeMessage(id);
|
|
997
|
+
this.patch({ error: cause });
|
|
998
|
+
} else this.fail(cause, generation);
|
|
999
|
+
return false;
|
|
1000
|
+
} finally {
|
|
1001
|
+
if (this.alive(generation)) {
|
|
1002
|
+
this.activeEdit = void 0;
|
|
1003
|
+
this.settleEditing(id);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
/** Merge a complete REST row for a known id through the live-row guards: a tombstoned id is dropped, and an older
|
|
1008
|
+
* revision (or timestamp) never overwrites the newer row already recorded. Recorded as a non-insert change, so a
|
|
1009
|
+
* reconcile keeps it only while the row is still in the fetched range.
|
|
1010
|
+
*/
|
|
1011
|
+
applyRow(message) {
|
|
1012
|
+
if (this.deleted.has(message.id)) return;
|
|
1013
|
+
this.record(message, this.changes.get(message.id)?.insert === true, ++this.revision, true);
|
|
1014
|
+
}
|
|
1015
|
+
/** The 409 path: one `getMessage` shows the conflicting content. Its row is merged through the guards and becomes
|
|
1016
|
+
* the new snapshot; a `MESSAGE_NOT_FOUND` answer removes the row and ends edit mode; another failure keeps the
|
|
1017
|
+
* snapshot. `error` carries the conflict (or the reload failure).
|
|
1018
|
+
*/
|
|
1019
|
+
async reloadConflict(id, conflict, generation) {
|
|
1020
|
+
try {
|
|
1021
|
+
const current = await this.client.getMessage(id);
|
|
1022
|
+
if (!this.alive(generation)) return;
|
|
1023
|
+
if (!this.validMessage(current) || current.id !== id) throw new Error("Complete message response does not match the edited message");
|
|
1024
|
+
this.applyRow(current);
|
|
1025
|
+
const row = this.state.messages.find((message) => message.id === id);
|
|
1026
|
+
const editing = this.state.editingMessage?.id === id && row ? { editingMessage: row } : {};
|
|
1027
|
+
this.patch({ ...editing, error: conflict });
|
|
1028
|
+
} catch (cause) {
|
|
1029
|
+
if (!this.alive(generation)) return;
|
|
1030
|
+
if (isTargetMiss(cause)) this.removeMessage(id);
|
|
1031
|
+
this.patch({ error: cause });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
/** Delete one of the caller's own confirmed messages (0.8.0). The row stays until the server answers: on success,
|
|
1035
|
+
* or when the server no longer knows it (`MESSAGE_NOT_FOUND`), it is tombstoned and removed (late responses, row
|
|
1036
|
+
* images and hydrations for it are dropped, the acknowledgement target is re-resolved and edit mode on it ends)
|
|
1037
|
+
* and the call resolves true; any other failure keeps the row, reports through `error` and resolves false. Rejects
|
|
1038
|
+
* when the adapter lacks `deleteMessage`.
|
|
1039
|
+
*/
|
|
1040
|
+
deleteMessage = async (messageId) => {
|
|
1041
|
+
const client = this.client;
|
|
1042
|
+
if (typeof client.deleteMessage !== "function") {
|
|
1043
|
+
throw new TypeError("This ConvoKitUiClient adapter does not implement deleteMessage (0.8)");
|
|
1044
|
+
}
|
|
1045
|
+
if (!this.alive() || !this.ownRow(messageId)) return false;
|
|
1046
|
+
const generation = this.generation;
|
|
1047
|
+
this.patch({ error: null });
|
|
1048
|
+
try {
|
|
1049
|
+
await client.deleteMessage(messageId);
|
|
1050
|
+
} catch (cause) {
|
|
1051
|
+
if (!this.alive(generation)) return false;
|
|
1052
|
+
if (!isMessageMissing(cause)) {
|
|
1053
|
+
this.fail(cause, generation);
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!this.alive(generation)) return false;
|
|
1058
|
+
this.removeMessage(messageId);
|
|
1059
|
+
return true;
|
|
1060
|
+
};
|
|
873
1061
|
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId, this.state.readPositionByUserId);
|
|
874
1062
|
};
|
|
875
1063
|
|
|
@@ -923,11 +1111,18 @@ function useConversation(options) {
|
|
|
923
1111
|
hasLoaded: field("hasLoaded"),
|
|
924
1112
|
error: field("error"),
|
|
925
1113
|
currentUserId: field("currentUserId"),
|
|
1114
|
+
editingMessage: field("editingMessage"),
|
|
1115
|
+
canEditMessages: field("canEditMessages"),
|
|
1116
|
+
canDeleteMessages: field("canDeleteMessages"),
|
|
926
1117
|
readerIdsFor: (message) => store.readerIdsFor(message),
|
|
927
1118
|
loadInitial: () => store.loadInitial(),
|
|
928
1119
|
refresh: () => store.refresh(),
|
|
929
1120
|
loadOlderMessages: () => store.loadOlderMessages(),
|
|
930
1121
|
sendMessage: (input) => store.sendMessage(input),
|
|
1122
|
+
startEditing: (messageId) => store.startEditing(messageId),
|
|
1123
|
+
cancelEditing: () => store.cancelEditing(),
|
|
1124
|
+
saveEdit: (text) => store.saveEdit(text),
|
|
1125
|
+
deleteMessage: (messageId) => store.deleteMessage(messageId),
|
|
931
1126
|
markRead: () => store.markRead(),
|
|
932
1127
|
updateTyping: (isTyping) => store.updateTyping(isTyping),
|
|
933
1128
|
setVisible: (value) => {
|
|
@@ -939,6 +1134,7 @@ function useConversation(options) {
|
|
|
939
1134
|
}
|
|
940
1135
|
|
|
941
1136
|
// src/components/message-list.ts
|
|
1137
|
+
import { isEditedMessage } from "@convokitapp/sdk";
|
|
942
1138
|
import {
|
|
943
1139
|
Check,
|
|
944
1140
|
CheckCheck,
|
|
@@ -948,7 +1144,9 @@ import {
|
|
|
948
1144
|
ImageOff,
|
|
949
1145
|
LoaderCircle,
|
|
950
1146
|
MapPin,
|
|
951
|
-
MessageCircle
|
|
1147
|
+
MessageCircle,
|
|
1148
|
+
Pencil,
|
|
1149
|
+
Trash2
|
|
952
1150
|
} from "@lucide/vue";
|
|
953
1151
|
import {
|
|
954
1152
|
computed as computed2,
|
|
@@ -1010,6 +1208,10 @@ var MessageListView = defineComponent2({
|
|
|
1010
1208
|
isLoadingOlder: { type: Boolean, default: false },
|
|
1011
1209
|
error: { type: null, required: false },
|
|
1012
1210
|
onAttachmentClick: { type: Function, default: void 0 },
|
|
1211
|
+
onEditMessage: { type: Function, default: void 0 },
|
|
1212
|
+
onDeleteMessage: { type: Function, default: void 0 },
|
|
1213
|
+
canEditMessage: { type: Function, default: void 0 },
|
|
1214
|
+
confirmDelete: { type: Function, default: void 0 },
|
|
1013
1215
|
scrollElement: { type: Object, default: void 0 },
|
|
1014
1216
|
paginationThreshold: { type: Number, default: 240 },
|
|
1015
1217
|
reverse: { type: Boolean, default: true },
|
|
@@ -1017,9 +1219,10 @@ var MessageListView = defineComponent2({
|
|
|
1017
1219
|
formatTime: { type: Function, default: formatMessageTime },
|
|
1018
1220
|
imageLoading: { type: String, default: "lazy" }
|
|
1019
1221
|
},
|
|
1020
|
-
emits: ["load-older", "attachment-click"],
|
|
1222
|
+
emits: ["load-older", "attachment-click", "edit-message", "delete-message"],
|
|
1021
1223
|
setup(props, { attrs, emit, slots }) {
|
|
1022
1224
|
const internalElement = ref(null);
|
|
1225
|
+
const confirming = ref(null);
|
|
1023
1226
|
let requestInFlight = false;
|
|
1024
1227
|
let lastRequestedLength = null;
|
|
1025
1228
|
let previousMessageCount = 0;
|
|
@@ -1059,16 +1262,77 @@ var MessageListView = defineComponent2({
|
|
|
1059
1262
|
}
|
|
1060
1263
|
}
|
|
1061
1264
|
}, { flush: "post", immediate: true });
|
|
1265
|
+
const viewerRole = computed2(() => props.conversation.membership?.role ?? props.conversation.participants.find((participant) => participant.appUserId === props.currentUserId || participant.id === props.currentUserId)?.role);
|
|
1266
|
+
const remove = async (message) => {
|
|
1267
|
+
if (props.confirmDelete && !await props.confirmDelete(message)) return false;
|
|
1268
|
+
return await props.onDeleteMessage?.(message) !== false;
|
|
1269
|
+
};
|
|
1062
1270
|
const renderMessage = (message, index) => {
|
|
1063
1271
|
const isCurrentUser = message.senderId === props.currentUserId;
|
|
1064
1272
|
const sender = participants.value.get(message.senderId);
|
|
1065
1273
|
const isPending = isConvoKitPendingMessage(message);
|
|
1066
1274
|
const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId, props.readPositionByUserId);
|
|
1067
|
-
const
|
|
1275
|
+
const isEdited = !isPending && isEditedMessage(message);
|
|
1276
|
+
const eligible = !isPending && (props.canEditMessage ? props.canEditMessage(message) : isCurrentUser && viewerRole.value !== "READ");
|
|
1277
|
+
const canEdit = eligible && !!props.onEditMessage;
|
|
1278
|
+
const canDelete = eligible && !!props.onDeleteMessage;
|
|
1279
|
+
const edit = () => {
|
|
1280
|
+
props.onEditMessage?.(message);
|
|
1281
|
+
};
|
|
1282
|
+
const slotProps = {
|
|
1283
|
+
message,
|
|
1284
|
+
chronologicalIndex: index,
|
|
1285
|
+
isCurrentUser,
|
|
1286
|
+
sender,
|
|
1287
|
+
readerIds,
|
|
1288
|
+
isEdited,
|
|
1289
|
+
canEdit,
|
|
1290
|
+
canDelete,
|
|
1291
|
+
...canEdit ? { edit } : {},
|
|
1292
|
+
...canDelete ? { remove: () => remove(message) } : {}
|
|
1293
|
+
};
|
|
1068
1294
|
const custom = slots.message?.(slotProps);
|
|
1069
1295
|
if (custom) return h2("div", { key: message.id, role: "listitem" }, custom);
|
|
1070
1296
|
const currentAppearance = appearance();
|
|
1071
1297
|
const messagePart = isCurrentUser ? "outgoingMessage" : "incomingMessage";
|
|
1298
|
+
const iconButton = (label, onClick, icon) => h2("button", {
|
|
1299
|
+
type: "button",
|
|
1300
|
+
"aria-label": label,
|
|
1301
|
+
onClick,
|
|
1302
|
+
class: partClass("button", currentAppearance, "ckui-icon-button"),
|
|
1303
|
+
style: partStyle("button", currentAppearance)
|
|
1304
|
+
}, [icon]);
|
|
1305
|
+
const actions = canEdit || canDelete ? h2("div", { class: "ckui-message-actions" }, [
|
|
1306
|
+
...canEdit ? [iconButton("Edit message", edit, h2(Pencil, { size: 16, "aria-hidden": "true" }))] : [],
|
|
1307
|
+
...canDelete ? [iconButton("Delete message", () => {
|
|
1308
|
+
if (props.confirmDelete) void remove(message);
|
|
1309
|
+
else confirming.value = message.id;
|
|
1310
|
+
}, h2(Trash2, { size: 16, "aria-hidden": "true" }))] : []
|
|
1311
|
+
]) : null;
|
|
1312
|
+
const confirm = canDelete && confirming.value === message.id ? h2("div", {
|
|
1313
|
+
class: "ckui-message-confirm",
|
|
1314
|
+
role: "group",
|
|
1315
|
+
"aria-label": "Delete this message?"
|
|
1316
|
+
}, [
|
|
1317
|
+
h2("span", "Delete this message?"),
|
|
1318
|
+
h2("button", {
|
|
1319
|
+
type: "button",
|
|
1320
|
+
class: "ckui-link-button",
|
|
1321
|
+
"aria-label": "Confirm delete",
|
|
1322
|
+
onClick: () => {
|
|
1323
|
+
confirming.value = null;
|
|
1324
|
+
void props.onDeleteMessage?.(message);
|
|
1325
|
+
}
|
|
1326
|
+
}, "Delete"),
|
|
1327
|
+
h2("button", {
|
|
1328
|
+
type: "button",
|
|
1329
|
+
class: "ckui-link-button",
|
|
1330
|
+
"aria-label": "Cancel delete",
|
|
1331
|
+
onClick: () => {
|
|
1332
|
+
confirming.value = null;
|
|
1333
|
+
}
|
|
1334
|
+
}, "Cancel")
|
|
1335
|
+
]) : null;
|
|
1072
1336
|
const mediaNodes = message.media.map((media, mediaIndex) => {
|
|
1073
1337
|
const open = props.onAttachmentClick ? () => {
|
|
1074
1338
|
props.onAttachmentClick?.(media, message);
|
|
@@ -1092,15 +1356,19 @@ var MessageListView = defineComponent2({
|
|
|
1092
1356
|
style: [props.styles?.message, props.styles?.[messagePart]],
|
|
1093
1357
|
"data-message-id": message.id
|
|
1094
1358
|
}, [
|
|
1359
|
+
// Spread, not null: an absent action/label/prompt must not leave a comment node (0.7 markup stays byte-identical).
|
|
1360
|
+
...actions ? [actions] : [],
|
|
1095
1361
|
h2("div", { class: "ckui-message-bubble" }, [
|
|
1096
1362
|
!isCurrentUser ? h2("strong", { class: "ckui-message-sender" }, sender?.name || message.senderId) : null,
|
|
1097
1363
|
message.text ? h2("div", { class: "ckui-message-text" }, message.text) : null,
|
|
1098
1364
|
...mediaNodes,
|
|
1099
1365
|
h2("span", { class: "ckui-message-time" }, [
|
|
1100
1366
|
isPending ? "Sending\u2026" : props.formatTime(message.createdAt),
|
|
1367
|
+
...isEdited ? [h2("span", { class: "ckui-message-edited", "aria-label": "Edited" }, "Edited")] : [],
|
|
1101
1368
|
isCurrentUser && !isPending ? readerIds.size > 0 ? h2(CheckCheck, { size: 14, "aria-label": "Read" }) : h2(Check, { size: 14, "aria-label": "Sent" }) : null
|
|
1102
1369
|
])
|
|
1103
1370
|
]),
|
|
1371
|
+
...confirm ? [confirm] : [],
|
|
1104
1372
|
isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? h2("div", {
|
|
1105
1373
|
class: partClass("receipt", currentAppearance, "ckui-read-receipt"),
|
|
1106
1374
|
style: partStyle("receipt", currentAppearance)
|
|
@@ -1189,6 +1457,13 @@ var viewProps = {
|
|
|
1189
1457
|
onTypingChange: { type: Function, default: void 0 },
|
|
1190
1458
|
onAddAttachment: { type: Function, default: void 0 },
|
|
1191
1459
|
onAttachmentClick: { type: Function, default: void 0 },
|
|
1460
|
+
editingMessage: { type: Object, default: null },
|
|
1461
|
+
onEditMessage: { type: Function, default: void 0 },
|
|
1462
|
+
onSaveEdit: { type: Function, default: void 0 },
|
|
1463
|
+
onCancelEdit: { type: Function, default: void 0 },
|
|
1464
|
+
onDeleteMessage: { type: Function, default: void 0 },
|
|
1465
|
+
canEditMessage: { type: Function, default: void 0 },
|
|
1466
|
+
confirmDelete: { type: Function, default: void 0 },
|
|
1192
1467
|
isInitialLoading: { type: Boolean, default: false },
|
|
1193
1468
|
isLoadingOlder: { type: Boolean, default: false },
|
|
1194
1469
|
isSending: { type: Boolean, default: false },
|
|
@@ -1208,6 +1483,9 @@ var viewProps = {
|
|
|
1208
1483
|
defaultDraft: { type: String, default: "" },
|
|
1209
1484
|
onDraftChange: { type: Function, default: void 0 }
|
|
1210
1485
|
};
|
|
1486
|
+
function editingSummary(message) {
|
|
1487
|
+
return message.text?.trim() || (message.media.length === 1 ? "1 attachment" : `${message.media.length} attachments`);
|
|
1488
|
+
}
|
|
1211
1489
|
function typingLabel(userIds, displayNameForUser) {
|
|
1212
1490
|
const names = [...userIds].map(displayNameForUser);
|
|
1213
1491
|
if (names.length === 0) return "";
|
|
@@ -1227,7 +1505,11 @@ var ConversationView = defineComponent3({
|
|
|
1227
1505
|
"load-older",
|
|
1228
1506
|
"add-attachment",
|
|
1229
1507
|
"attachment-click",
|
|
1230
|
-
"update:modelValue"
|
|
1508
|
+
"update:modelValue",
|
|
1509
|
+
"edit-message",
|
|
1510
|
+
"save-edit",
|
|
1511
|
+
"cancel-edit",
|
|
1512
|
+
"delete-message"
|
|
1231
1513
|
],
|
|
1232
1514
|
setup(props, { attrs, emit, slots }) {
|
|
1233
1515
|
const internalDraft = ref2(props.defaultDraft);
|
|
@@ -1240,15 +1522,57 @@ var ConversationView = defineComponent3({
|
|
|
1240
1522
|
});
|
|
1241
1523
|
const draft = () => props.modelValue ?? internalDraft.value;
|
|
1242
1524
|
let latestDraft = draft();
|
|
1243
|
-
|
|
1525
|
+
let stash;
|
|
1526
|
+
let saving = false;
|
|
1527
|
+
const setDraft = (value, typing = true) => {
|
|
1244
1528
|
latestDraft = value;
|
|
1245
1529
|
if (props.modelValue === void 0) internalDraft.value = value;
|
|
1246
1530
|
props.onDraftChange?.(value);
|
|
1247
1531
|
emit("update:modelValue", value);
|
|
1248
|
-
|
|
1249
|
-
|
|
1532
|
+
if (typing) void props.onTypingChange?.(value.trim().length > 0);
|
|
1533
|
+
};
|
|
1534
|
+
const enterEdit = (message) => {
|
|
1535
|
+
if (stash === void 0) stash = draft();
|
|
1536
|
+
setDraft(message.text ?? "", false);
|
|
1537
|
+
};
|
|
1538
|
+
const leaveEdit = (message, restore) => {
|
|
1539
|
+
if (stash === void 0) return;
|
|
1540
|
+
const saved = stash;
|
|
1541
|
+
stash = void 0;
|
|
1542
|
+
const current = draft();
|
|
1543
|
+
if (restore === "always" || current.trim() === "" || current === (message.text ?? "")) setDraft(saved);
|
|
1250
1544
|
};
|
|
1545
|
+
watch3(() => props.editingMessage, (next, previous) => {
|
|
1546
|
+
if (next && (!previous || previous.id !== next.id)) enterEdit(next);
|
|
1547
|
+
else if (!next && previous && !saving) leaveEdit(previous, "unchanged");
|
|
1548
|
+
}, { immediate: true });
|
|
1549
|
+
const cancelEdit = () => {
|
|
1550
|
+
const editing = props.editingMessage;
|
|
1551
|
+
if (!editing) return;
|
|
1552
|
+
leaveEdit(editing, "always");
|
|
1553
|
+
props.onCancelEdit?.();
|
|
1554
|
+
};
|
|
1555
|
+
const canSave = (editing) => draft().trim().length > 0 || editing.media.length > 0;
|
|
1251
1556
|
const submit = async () => {
|
|
1557
|
+
const editing = props.editingMessage;
|
|
1558
|
+
if (editing) {
|
|
1559
|
+
const text2 = draft().trim();
|
|
1560
|
+
if (!canSave(editing) || props.isSending || submitting.value) return;
|
|
1561
|
+
submitting.value = true;
|
|
1562
|
+
saving = true;
|
|
1563
|
+
try {
|
|
1564
|
+
const saved = await props.onSaveEdit?.(editing, text2);
|
|
1565
|
+
if (saved === false) {
|
|
1566
|
+
if (props.editingMessage === null) leaveEdit(editing, "unchanged");
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
leaveEdit(editing, "always");
|
|
1570
|
+
} finally {
|
|
1571
|
+
saving = false;
|
|
1572
|
+
submitting.value = false;
|
|
1573
|
+
}
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1252
1576
|
const originalDraft = draft();
|
|
1253
1577
|
const text = originalDraft.trim();
|
|
1254
1578
|
if (!text || props.isSending || submitting.value) return;
|
|
@@ -1317,6 +1641,7 @@ var ConversationView = defineComponent3({
|
|
|
1317
1641
|
}, typingLabel(props.typingUserIds, nameForUser));
|
|
1318
1642
|
};
|
|
1319
1643
|
const renderComposer = () => {
|
|
1644
|
+
const editing = props.editingMessage;
|
|
1320
1645
|
const slotProps = {
|
|
1321
1646
|
value: draft(),
|
|
1322
1647
|
setValue: setDraft,
|
|
@@ -1324,16 +1649,27 @@ var ConversationView = defineComponent3({
|
|
|
1324
1649
|
send: () => {
|
|
1325
1650
|
void submit();
|
|
1326
1651
|
},
|
|
1327
|
-
...props.onAddAttachment ? { addAttachment } : {}
|
|
1652
|
+
...props.onAddAttachment ? { addAttachment } : {},
|
|
1653
|
+
...editing ? { editing, cancelEdit } : {}
|
|
1328
1654
|
};
|
|
1655
|
+
const busy = props.isSending || submitting.value;
|
|
1329
1656
|
return slots.composer?.(slotProps) ?? h3("form", {
|
|
1330
|
-
class: partClass("composer", appearance(), "ckui-composer"),
|
|
1657
|
+
class: cx(partClass("composer", appearance(), "ckui-composer"), editing && !props.unstyled && "ckui-composer--editing"),
|
|
1331
1658
|
style: partStyle("composer", appearance()),
|
|
1332
1659
|
onSubmit: (event) => {
|
|
1333
1660
|
event.preventDefault();
|
|
1334
1661
|
void submit();
|
|
1335
1662
|
}
|
|
1336
1663
|
}, [
|
|
1664
|
+
// Spread, not null: outside edit mode the composer markup stays byte-identical to 0.7.
|
|
1665
|
+
...editing ? [h3("div", { class: "ckui-composer__editing", role: "status" }, [
|
|
1666
|
+
h3(Pencil2, { size: 14, "aria-hidden": "true" }),
|
|
1667
|
+
h3("span", { class: "ckui-composer__editing-body" }, [
|
|
1668
|
+
h3("strong", "Editing message"),
|
|
1669
|
+
h3("span", editingSummary(editing))
|
|
1670
|
+
]),
|
|
1671
|
+
h3("button", { type: "button", class: "ckui-link-button", "aria-label": "Cancel editing", onClick: cancelEdit }, "Cancel")
|
|
1672
|
+
])] : [],
|
|
1337
1673
|
props.onAddAttachment ? h3("button", {
|
|
1338
1674
|
type: "button",
|
|
1339
1675
|
"aria-label": "Add attachment",
|
|
@@ -1362,15 +1698,19 @@ var ConversationView = defineComponent3({
|
|
|
1362
1698
|
event.preventDefault();
|
|
1363
1699
|
void submit();
|
|
1364
1700
|
}
|
|
1701
|
+
if (event.key === "Escape" && props.editingMessage) {
|
|
1702
|
+
event.preventDefault();
|
|
1703
|
+
cancelEdit();
|
|
1704
|
+
}
|
|
1365
1705
|
}
|
|
1366
1706
|
}),
|
|
1367
1707
|
h3("button", {
|
|
1368
1708
|
type: "submit",
|
|
1369
|
-
"aria-label": "Send message",
|
|
1370
|
-
disabled: !draft().trim() ||
|
|
1709
|
+
"aria-label": editing ? "Save message" : "Send message",
|
|
1710
|
+
disabled: (editing ? !canSave(editing) : !draft().trim()) || busy,
|
|
1371
1711
|
class: partClass("button", appearance(), "ckui-send-button"),
|
|
1372
1712
|
style: partStyle("button", appearance())
|
|
1373
|
-
}, [
|
|
1713
|
+
}, [busy ? h3(LoaderCircle2, { class: "ckui-spin", size: 18, "aria-hidden": "true" }) : h3(editing ? Check2 : Send, { size: 18, "aria-hidden": "true" })])
|
|
1374
1714
|
]);
|
|
1375
1715
|
};
|
|
1376
1716
|
return () => {
|
|
@@ -1419,6 +1759,12 @@ var ConversationView = defineComponent3({
|
|
|
1419
1759
|
...props.onAttachmentClick ? { onAttachmentClick: (media, message) => {
|
|
1420
1760
|
props.onAttachmentClick?.(media, message);
|
|
1421
1761
|
} } : {},
|
|
1762
|
+
...props.onEditMessage ? { onEditMessage: (message) => {
|
|
1763
|
+
props.onEditMessage?.(message);
|
|
1764
|
+
} } : {},
|
|
1765
|
+
...props.onDeleteMessage ? { onDeleteMessage: (message) => props.onDeleteMessage?.(message) } : {},
|
|
1766
|
+
...props.canEditMessage ? { canEditMessage: props.canEditMessage } : {},
|
|
1767
|
+
...props.confirmDelete ? { confirmDelete: props.confirmDelete } : {},
|
|
1422
1768
|
reverse: props.reverseMessages,
|
|
1423
1769
|
stickToBottom: props.stickToBottom,
|
|
1424
1770
|
paginationThreshold: props.paginationThreshold,
|
|
@@ -1449,6 +1795,11 @@ var Conversation = defineComponent3({
|
|
|
1449
1795
|
messages: { type: Array, default: () => [] },
|
|
1450
1796
|
currentUserId: { type: String, default: "" },
|
|
1451
1797
|
onSendMessage: { type: Function, default: void 0 },
|
|
1798
|
+
editingMessage: { type: Object, default: void 0 },
|
|
1799
|
+
onEditMessage: { type: Function, default: void 0 },
|
|
1800
|
+
onSaveEdit: { type: Function, default: void 0 },
|
|
1801
|
+
onCancelEdit: { type: Function, default: void 0 },
|
|
1802
|
+
onDeleteMessage: { type: Function, default: void 0 },
|
|
1452
1803
|
client: { type: Object, required: true },
|
|
1453
1804
|
conversationId: { type: String, required: true },
|
|
1454
1805
|
messagePageSize: { type: Number, default: 30 },
|
|
@@ -1458,7 +1809,21 @@ var Conversation = defineComponent3({
|
|
|
1458
1809
|
autoLoad: { type: Boolean, default: true },
|
|
1459
1810
|
onControllerChange: { type: Function, default: void 0 }
|
|
1460
1811
|
},
|
|
1461
|
-
emits: [
|
|
1812
|
+
emits: [
|
|
1813
|
+
"controller-change",
|
|
1814
|
+
"send-message",
|
|
1815
|
+
"typing-change",
|
|
1816
|
+
"back",
|
|
1817
|
+
"refresh",
|
|
1818
|
+
"load-older",
|
|
1819
|
+
"add-attachment",
|
|
1820
|
+
"attachment-click",
|
|
1821
|
+
"update:modelValue",
|
|
1822
|
+
"edit-message",
|
|
1823
|
+
"save-edit",
|
|
1824
|
+
"cancel-edit",
|
|
1825
|
+
"delete-message"
|
|
1826
|
+
],
|
|
1462
1827
|
setup(props, { attrs, emit, expose, slots }) {
|
|
1463
1828
|
const controller = useConversation({
|
|
1464
1829
|
client: () => props.client,
|
|
@@ -1522,6 +1887,11 @@ var Conversation = defineComponent3({
|
|
|
1522
1887
|
isSending: _isSending,
|
|
1523
1888
|
hasOlderMessages: _hasOlderMessages,
|
|
1524
1889
|
error: _error,
|
|
1890
|
+
editingMessage: _editingMessage,
|
|
1891
|
+
onEditMessage: _onEditMessage,
|
|
1892
|
+
onSaveEdit: _onSaveEdit,
|
|
1893
|
+
onCancelEdit: _onCancelEdit,
|
|
1894
|
+
onDeleteMessage: _onDeleteMessage,
|
|
1525
1895
|
...forwarded
|
|
1526
1896
|
} = props;
|
|
1527
1897
|
return h3(ConversationView, {
|
|
@@ -1545,6 +1915,28 @@ var Conversation = defineComponent3({
|
|
|
1545
1915
|
isSending: controller.isSending.value,
|
|
1546
1916
|
hasOlderMessages: controller.hasOlderMessages.value,
|
|
1547
1917
|
...controller.error.value == null ? {} : { error: controller.error.value },
|
|
1918
|
+
// Edit mode is the store's; the actions render only while the adapter supports them (0.8.0).
|
|
1919
|
+
editingMessage: controller.editingMessage.value,
|
|
1920
|
+
...controller.canEditMessages.value ? {
|
|
1921
|
+
onEditMessage: (message) => {
|
|
1922
|
+
emit("edit-message", message);
|
|
1923
|
+
controller.startEditing(message.id);
|
|
1924
|
+
},
|
|
1925
|
+
onSaveEdit: async (message, text) => {
|
|
1926
|
+
emit("save-edit", message, text);
|
|
1927
|
+
return controller.saveEdit(text);
|
|
1928
|
+
},
|
|
1929
|
+
onCancelEdit: () => {
|
|
1930
|
+
emit("cancel-edit");
|
|
1931
|
+
controller.cancelEditing();
|
|
1932
|
+
}
|
|
1933
|
+
} : {},
|
|
1934
|
+
...controller.canDeleteMessages.value ? {
|
|
1935
|
+
onDeleteMessage: async (message) => {
|
|
1936
|
+
emit("delete-message", message);
|
|
1937
|
+
return controller.deleteMessage(message.id);
|
|
1938
|
+
}
|
|
1939
|
+
} : {},
|
|
1548
1940
|
"onUpdate:modelValue": (value) => emit("update:modelValue", value),
|
|
1549
1941
|
...props.onBack ? { onBack: () => {
|
|
1550
1942
|
props.onBack?.();
|
|
@@ -1573,7 +1965,7 @@ import {
|
|
|
1573
1965
|
} from "vue";
|
|
1574
1966
|
|
|
1575
1967
|
// src/composables/use-conversation-list.ts
|
|
1576
|
-
import { computed as computed3, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, shallowRef as shallowRef2, toValue as toValue2, watch as
|
|
1968
|
+
import { computed as computed3, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, shallowRef as shallowRef2, toValue as toValue2, watch as watch4 } from "vue";
|
|
1577
1969
|
|
|
1578
1970
|
// src/conversation-list-store.ts
|
|
1579
1971
|
var defaultActivityRefreshWindowMs = 500;
|
|
@@ -2031,7 +2423,7 @@ function useConversationList(options) {
|
|
|
2031
2423
|
let store = createStore();
|
|
2032
2424
|
const snapshot = shallowRef2(store.getSnapshot());
|
|
2033
2425
|
let unsubscribe;
|
|
2034
|
-
const stop =
|
|
2426
|
+
const stop = watch4(
|
|
2035
2427
|
() => [toValue2(options.client), toValue2(options.client).sessionIdentity],
|
|
2036
2428
|
() => {
|
|
2037
2429
|
store.dispose();
|