@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.cjs
CHANGED
|
@@ -72,6 +72,8 @@ function createConvoKitUiClient(client) {
|
|
|
72
72
|
getMessages: (options) => client.getMessages(options),
|
|
73
73
|
getMessage: (id) => client.getMessage(id),
|
|
74
74
|
sendMessage: (input) => client.sendMessage(input),
|
|
75
|
+
editMessage: (messageId, input) => client.editMessage(messageId, input),
|
|
76
|
+
deleteMessage: (messageId) => client.deleteMessage(messageId),
|
|
75
77
|
markConversationRead: (conversationId, options) => client.markConversationRead(conversationId, options),
|
|
76
78
|
markConversationUnread: (conversationId) => client.markConversationUnread(conversationId),
|
|
77
79
|
clearConversationUnread: (conversationId, options) => client.clearConversationUnread(conversationId, options),
|
|
@@ -246,9 +248,31 @@ function version(message) {
|
|
|
246
248
|
function hasContent(message) {
|
|
247
249
|
return !!message.text?.trim() || message.media.length > 0;
|
|
248
250
|
}
|
|
251
|
+
function revisionOrder(left, right) {
|
|
252
|
+
const a = left.revision, b = right.revision;
|
|
253
|
+
if (typeof a !== "number" || typeof b !== "number" || a <= 0 && b <= 0) return void 0;
|
|
254
|
+
return a === b ? void 0 : a - b;
|
|
255
|
+
}
|
|
256
|
+
function older(candidate, reference) {
|
|
257
|
+
const byRevision = revisionOrder(candidate, reference);
|
|
258
|
+
return byRevision === void 0 ? version(candidate) < version(reference) : byRevision < 0;
|
|
259
|
+
}
|
|
249
260
|
function newest(current, incoming, incomingComplete = true) {
|
|
261
|
+
const byRevision = revisionOrder(current, incoming);
|
|
262
|
+
if (byRevision !== void 0) return byRevision > 0 ? current : incoming;
|
|
250
263
|
return version(current) > version(incoming) || !incomingComplete && version(current) === version(incoming) ? current : incoming;
|
|
251
264
|
}
|
|
265
|
+
function isRevisionConflict(cause) {
|
|
266
|
+
if (typeof cause !== "object" || cause === null) return false;
|
|
267
|
+
const { code, status } = cause;
|
|
268
|
+
return code === "REVISION_CONFLICT" || code === void 0 && status === 409;
|
|
269
|
+
}
|
|
270
|
+
function isMessageMissing(cause) {
|
|
271
|
+
return typeof cause === "object" && cause !== null && cause.code === "MESSAGE_NOT_FOUND";
|
|
272
|
+
}
|
|
273
|
+
function localConflict() {
|
|
274
|
+
return Object.assign(new Error("Message was changed since it was loaded"), { code: "REVISION_CONFLICT" });
|
|
275
|
+
}
|
|
252
276
|
var compare = compareMessageOrder;
|
|
253
277
|
function positionCursor(position) {
|
|
254
278
|
return { createdAt: position.createdAt, id: position.messageId };
|
|
@@ -268,7 +292,7 @@ function isTargetMiss(cause) {
|
|
|
268
292
|
const { code, status } = cause;
|
|
269
293
|
return code === "MESSAGE_NOT_FOUND" || code === void 0 && status === 404;
|
|
270
294
|
}
|
|
271
|
-
function blank(currentUserId = "") {
|
|
295
|
+
function blank(currentUserId = "", support = { edit: false, delete: false }) {
|
|
272
296
|
return {
|
|
273
297
|
conversation: null,
|
|
274
298
|
messages: [],
|
|
@@ -282,7 +306,10 @@ function blank(currentUserId = "") {
|
|
|
282
306
|
hasOlderMessages: true,
|
|
283
307
|
hasLoaded: false,
|
|
284
308
|
error: null,
|
|
285
|
-
currentUserId
|
|
309
|
+
currentUserId,
|
|
310
|
+
editingMessage: null,
|
|
311
|
+
canEditMessages: support.edit,
|
|
312
|
+
canDeleteMessages: support.delete
|
|
286
313
|
};
|
|
287
314
|
}
|
|
288
315
|
var ConversationStore = class {
|
|
@@ -301,7 +328,8 @@ var ConversationStore = class {
|
|
|
301
328
|
}
|
|
302
329
|
this.owner = this.client.sessionIdentity;
|
|
303
330
|
this.user = this.owner ? this.client.currentUserId : "";
|
|
304
|
-
this.
|
|
331
|
+
this.support = { edit: typeof this.client.editMessage === "function", delete: typeof this.client.deleteMessage === "function" };
|
|
332
|
+
this.state = blank(this.user, this.support);
|
|
305
333
|
}
|
|
306
334
|
options;
|
|
307
335
|
client;
|
|
@@ -328,6 +356,13 @@ var ConversationStore = class {
|
|
|
328
356
|
visible = true;
|
|
329
357
|
sendRevision;
|
|
330
358
|
activeSend;
|
|
359
|
+
/** Adapter support for author edits/deletes, decided once like `listInbox`. */
|
|
360
|
+
support;
|
|
361
|
+
/** The id whose `saveEdit` request is in flight: its outcome (success or 409) decides the edit, so newer rows for
|
|
362
|
+
* it arriving meanwhile (its own UPDATE image, typically) are not reported as a local conflict while it lasts;
|
|
363
|
+
* `settleEditing` re-evaluates them once the request has settled any other way.
|
|
364
|
+
*/
|
|
365
|
+
activeEdit;
|
|
331
366
|
refreshQueued = false;
|
|
332
367
|
typingTimers = /* @__PURE__ */ new Map();
|
|
333
368
|
ownTypingTimer;
|
|
@@ -346,9 +381,33 @@ var ConversationStore = class {
|
|
|
346
381
|
for (const message of patch.messages) this.confirmSend(message);
|
|
347
382
|
if (this.activeSend.confirmed) patch.messages = patch.messages.filter((message) => message.id !== this.activeSend.pending.id);
|
|
348
383
|
}
|
|
384
|
+
if (patch.messages) patch = this.trackEditing(patch);
|
|
349
385
|
this.state = { ...this.state, ...patch };
|
|
350
386
|
for (const listener of this.listeners) listener();
|
|
351
387
|
}
|
|
388
|
+
/** Edit mode follows the edited row wherever a message list reaches the state: the row leaving the list (deletion,
|
|
389
|
+
* reconcile tombstone, eviction) ends it, and a row for it with a higher revision than the snapshot (UPDATE image,
|
|
390
|
+
* hydration, reconcile, refresh) is the local conflict: the snapshot is replaced and `error` carries the conflict
|
|
391
|
+
* code, without a request. A save in flight owns its own outcome (`activeEdit`) and re-checks when it settles
|
|
392
|
+
* (`settleEditing`).
|
|
393
|
+
*/
|
|
394
|
+
trackEditing(patch) {
|
|
395
|
+
const editing = patch.editingMessage === void 0 ? this.state.editingMessage : patch.editingMessage;
|
|
396
|
+
if (!editing || !patch.messages) return patch;
|
|
397
|
+
const live = patch.messages.find((message) => message.id === editing.id);
|
|
398
|
+
if (!live) return { ...patch, editingMessage: null };
|
|
399
|
+
if (this.activeEdit === editing.id || !(live.revision > editing.revision)) return patch;
|
|
400
|
+
return { ...patch, editingMessage: live, error: localConflict() };
|
|
401
|
+
}
|
|
402
|
+
/** After a save for `id` has settled without deciding the edit (a failure, or a 409 whose reload failed), a newer
|
|
403
|
+
* row for it that arrived during the request is the local conflict after all: the snapshot is replaced and
|
|
404
|
+
* `error` carries the conflict code, so the next save carries the fresh revision without another round trip.
|
|
405
|
+
*/
|
|
406
|
+
settleEditing(id) {
|
|
407
|
+
const editing = this.state.editingMessage;
|
|
408
|
+
const live = editing?.id === id ? this.state.messages.find((message) => message.id === id) : void 0;
|
|
409
|
+
if (editing && live && live.revision > editing.revision) this.patch({ editingMessage: live, error: localConflict() });
|
|
410
|
+
}
|
|
352
411
|
alive(generation = this.generation) {
|
|
353
412
|
return !this.disposed && generation === this.generation && this.owner !== null && this.client.sessionIdentity === this.owner;
|
|
354
413
|
}
|
|
@@ -392,6 +451,7 @@ var ConversationStore = class {
|
|
|
392
451
|
this.captured = capture();
|
|
393
452
|
this.sendRevision = void 0;
|
|
394
453
|
this.activeSend = void 0;
|
|
454
|
+
this.activeEdit = void 0;
|
|
395
455
|
this.refreshQueued = false;
|
|
396
456
|
}
|
|
397
457
|
dispose = () => {
|
|
@@ -400,14 +460,14 @@ var ConversationStore = class {
|
|
|
400
460
|
}
|
|
401
461
|
this.disposed = true;
|
|
402
462
|
this.clear();
|
|
403
|
-
this.patch(blank());
|
|
463
|
+
this.patch(blank("", this.support));
|
|
404
464
|
};
|
|
405
465
|
fail(cause, generation, history = false) {
|
|
406
466
|
if (!this.alive(generation)) return;
|
|
407
467
|
const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
408
468
|
if (history && (status === 401 || status === 403 || status === 404)) {
|
|
409
469
|
this.clear();
|
|
410
|
-
this.patch({ ...blank(this.user), error: cause, hasLoaded: true, hasOlderMessages: false });
|
|
470
|
+
this.patch({ ...blank(this.user, this.support), error: cause, hasLoaded: true, hasOlderMessages: false });
|
|
411
471
|
} else this.patch({ error: cause });
|
|
412
472
|
}
|
|
413
473
|
attach(generation, data = true) {
|
|
@@ -479,7 +539,7 @@ var ConversationStore = class {
|
|
|
479
539
|
if (!this.alive(generation) || type !== "insert" && type !== "update" || !this.validMessage(message) || this.deleted.has(message.id)) return;
|
|
480
540
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
481
541
|
const known = existing ?? this.changes.get(message.id)?.message;
|
|
482
|
-
if (known &&
|
|
542
|
+
if (known && older(message, known)) return;
|
|
483
543
|
const insert = type === "insert" || this.changes.get(message.id)?.insert === true;
|
|
484
544
|
if (!existing && !insert && type === "update" && !this.state.isInitialLoading && !this.state.isLoadingOlder && !this.state.isReconciling) return;
|
|
485
545
|
const revision = ++this.revision;
|
|
@@ -492,7 +552,7 @@ var ConversationStore = class {
|
|
|
492
552
|
}
|
|
493
553
|
record(message, insert, revision, complete) {
|
|
494
554
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
495
|
-
if (existing &&
|
|
555
|
+
if (existing && older(message, existing)) return;
|
|
496
556
|
if (this.confirmSend(message) && !complete && !message.media.length) {
|
|
497
557
|
message = { ...message, media: this.activeSend.pending.media };
|
|
498
558
|
this.confirmSend(message);
|
|
@@ -543,7 +603,7 @@ var ConversationStore = class {
|
|
|
543
603
|
if (!this.currentHydration(job)) return;
|
|
544
604
|
const full = await this.client.getMessage(id);
|
|
545
605
|
if (!this.currentHydration(job)) return;
|
|
546
|
-
if (!this.validMessage(full) || full.id !== id || full.senderId !== job.message.senderId ||
|
|
606
|
+
if (!this.validMessage(full) || full.id !== id || full.senderId !== job.message.senderId || older(full, job.message)) {
|
|
547
607
|
throw new Error("Complete message response does not match the observed resource/revision");
|
|
548
608
|
}
|
|
549
609
|
this.record(full, job.insert, job.revision, true);
|
|
@@ -623,7 +683,7 @@ var ConversationStore = class {
|
|
|
623
683
|
if (!this.alive()) return;
|
|
624
684
|
this.clear();
|
|
625
685
|
const generation = this.generation;
|
|
626
|
-
this.patch({ ...blank(this.user), isInitialLoading: true });
|
|
686
|
+
this.patch({ ...blank(this.user, this.support), isInitialLoading: true });
|
|
627
687
|
const revision = this.revision;
|
|
628
688
|
try {
|
|
629
689
|
this.attach(generation);
|
|
@@ -866,7 +926,8 @@ var ConversationStore = class {
|
|
|
866
926
|
text: normalized || null,
|
|
867
927
|
media: media ?? [],
|
|
868
928
|
createdAt: /* @__PURE__ */ new Date(),
|
|
869
|
-
updatedAt: null
|
|
929
|
+
updatedAt: null,
|
|
930
|
+
revision: 0
|
|
870
931
|
};
|
|
871
932
|
const send = { pending };
|
|
872
933
|
this.activeSend = send;
|
|
@@ -910,6 +971,132 @@ var ConversationStore = class {
|
|
|
910
971
|
}
|
|
911
972
|
}
|
|
912
973
|
};
|
|
974
|
+
/** The caller's role in the open room when known (0.7 `membership`, else the caller's participant row). */
|
|
975
|
+
ownRole() {
|
|
976
|
+
const conversation = this.state.conversation;
|
|
977
|
+
return conversation?.membership?.role ?? conversation?.participants.find((participant) => participant.appUserId === this.user || participant.id === this.user)?.role;
|
|
978
|
+
}
|
|
979
|
+
/** A rendered, confirmed row of the caller's own that is not known to be gone. */
|
|
980
|
+
ownRow(messageId) {
|
|
981
|
+
const row = this.state.messages.find((message) => message.id === messageId);
|
|
982
|
+
return row && row.senderId === this.user && !isConvoKitPendingMessage(row) && !this.deleted.has(messageId) ? row : void 0;
|
|
983
|
+
}
|
|
984
|
+
/** Enter edit mode on one of the caller's own confirmed messages (0.8.0): the row as it stands now becomes the
|
|
985
|
+
* snapshot whose `revision` every save sends. A no-op unless the adapter implements `editMessage`, the row is
|
|
986
|
+
* rendered, own, confirmed, not tombstoned and the caller's role (when known) is not `READ`. Sends nothing.
|
|
987
|
+
*/
|
|
988
|
+
startEditing = (messageId) => {
|
|
989
|
+
if (!this.alive() || !this.support.edit || this.ownRole() === "READ") return;
|
|
990
|
+
const row = this.ownRow(messageId);
|
|
991
|
+
if (row) this.patch({ editingMessage: row });
|
|
992
|
+
};
|
|
993
|
+
/** Leave edit mode without a request; the draft is the view's to restore. */
|
|
994
|
+
cancelEditing = () => {
|
|
995
|
+
if (this.state.editingMessage) this.patch({ editingMessage: null });
|
|
996
|
+
};
|
|
997
|
+
/** Save the edit in progress with the snapshot's revision (never the live row's), trimming the text and sending
|
|
998
|
+
* `null` for an empty caption. Resolves true when the server accepted the edit (the response is merged through the
|
|
999
|
+
* tombstone and precedence guards and edit mode ends); false when nothing was saved: a stale revision (409
|
|
1000
|
+
* `REVISION_CONFLICT`) reloads the row once through `getMessage`, replaces the snapshot with it (the next save
|
|
1001
|
+
* carries the fresh revision) and reports the conflict through `error`, keeping edit mode; a coded 404
|
|
1002
|
+
* (`MESSAGE_NOT_FOUND`, on the save or on that reload) removes the row and ends edit mode; any other failure
|
|
1003
|
+
* (403, 500, network, an uncoded 404 from a 0.7 backend) is reported through `error` without evicting anything and
|
|
1004
|
+
* keeps edit mode; if a newer row for the message arrived during such a request, that row is then the local
|
|
1005
|
+
* conflict (`settleEditing`). A text-only message cannot be saved empty (no request). Rejects when the adapter
|
|
1006
|
+
* lacks `editMessage`.
|
|
1007
|
+
*/
|
|
1008
|
+
saveEdit = async (text) => {
|
|
1009
|
+
const client = this.client;
|
|
1010
|
+
if (typeof client.editMessage !== "function") {
|
|
1011
|
+
throw new TypeError("This ConvoKitUiClient adapter does not implement editMessage (0.8)");
|
|
1012
|
+
}
|
|
1013
|
+
const snapshot = this.state.editingMessage;
|
|
1014
|
+
if (!this.alive() || !snapshot || this.activeEdit !== void 0) return false;
|
|
1015
|
+
const trimmed = text.trim();
|
|
1016
|
+
const normalized = trimmed === "" ? null : trimmed;
|
|
1017
|
+
if (normalized === null && snapshot.media.length === 0) return false;
|
|
1018
|
+
const generation = this.generation;
|
|
1019
|
+
const id = snapshot.id;
|
|
1020
|
+
this.activeEdit = id;
|
|
1021
|
+
this.patch({ error: null });
|
|
1022
|
+
try {
|
|
1023
|
+
const message = await client.editMessage(id, { text: normalized, revision: snapshot.revision });
|
|
1024
|
+
if (!this.alive(generation)) return false;
|
|
1025
|
+
if (!this.validMessage(message) || message.id !== id || message.senderId !== this.user) {
|
|
1026
|
+
throw new Error("Edit response belongs to a different message or sender");
|
|
1027
|
+
}
|
|
1028
|
+
if (this.state.editingMessage?.id === id) this.patch({ editingMessage: null });
|
|
1029
|
+
this.applyRow(message);
|
|
1030
|
+
return true;
|
|
1031
|
+
} catch (cause) {
|
|
1032
|
+
if (!this.alive(generation)) return false;
|
|
1033
|
+
if (isRevisionConflict(cause)) await this.reloadConflict(id, cause, generation);
|
|
1034
|
+
else if (isMessageMissing(cause)) {
|
|
1035
|
+
this.removeMessage(id);
|
|
1036
|
+
this.patch({ error: cause });
|
|
1037
|
+
} else this.fail(cause, generation);
|
|
1038
|
+
return false;
|
|
1039
|
+
} finally {
|
|
1040
|
+
if (this.alive(generation)) {
|
|
1041
|
+
this.activeEdit = void 0;
|
|
1042
|
+
this.settleEditing(id);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
/** Merge a complete REST row for a known id through the live-row guards: a tombstoned id is dropped, and an older
|
|
1047
|
+
* revision (or timestamp) never overwrites the newer row already recorded. Recorded as a non-insert change, so a
|
|
1048
|
+
* reconcile keeps it only while the row is still in the fetched range.
|
|
1049
|
+
*/
|
|
1050
|
+
applyRow(message) {
|
|
1051
|
+
if (this.deleted.has(message.id)) return;
|
|
1052
|
+
this.record(message, this.changes.get(message.id)?.insert === true, ++this.revision, true);
|
|
1053
|
+
}
|
|
1054
|
+
/** The 409 path: one `getMessage` shows the conflicting content. Its row is merged through the guards and becomes
|
|
1055
|
+
* the new snapshot; a `MESSAGE_NOT_FOUND` answer removes the row and ends edit mode; another failure keeps the
|
|
1056
|
+
* snapshot. `error` carries the conflict (or the reload failure).
|
|
1057
|
+
*/
|
|
1058
|
+
async reloadConflict(id, conflict, generation) {
|
|
1059
|
+
try {
|
|
1060
|
+
const current = await this.client.getMessage(id);
|
|
1061
|
+
if (!this.alive(generation)) return;
|
|
1062
|
+
if (!this.validMessage(current) || current.id !== id) throw new Error("Complete message response does not match the edited message");
|
|
1063
|
+
this.applyRow(current);
|
|
1064
|
+
const row = this.state.messages.find((message) => message.id === id);
|
|
1065
|
+
const editing = this.state.editingMessage?.id === id && row ? { editingMessage: row } : {};
|
|
1066
|
+
this.patch({ ...editing, error: conflict });
|
|
1067
|
+
} catch (cause) {
|
|
1068
|
+
if (!this.alive(generation)) return;
|
|
1069
|
+
if (isTargetMiss(cause)) this.removeMessage(id);
|
|
1070
|
+
this.patch({ error: cause });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/** Delete one of the caller's own confirmed messages (0.8.0). The row stays until the server answers: on success,
|
|
1074
|
+
* or when the server no longer knows it (`MESSAGE_NOT_FOUND`), it is tombstoned and removed (late responses, row
|
|
1075
|
+
* images and hydrations for it are dropped, the acknowledgement target is re-resolved and edit mode on it ends)
|
|
1076
|
+
* and the call resolves true; any other failure keeps the row, reports through `error` and resolves false. Rejects
|
|
1077
|
+
* when the adapter lacks `deleteMessage`.
|
|
1078
|
+
*/
|
|
1079
|
+
deleteMessage = async (messageId) => {
|
|
1080
|
+
const client = this.client;
|
|
1081
|
+
if (typeof client.deleteMessage !== "function") {
|
|
1082
|
+
throw new TypeError("This ConvoKitUiClient adapter does not implement deleteMessage (0.8)");
|
|
1083
|
+
}
|
|
1084
|
+
if (!this.alive() || !this.ownRow(messageId)) return false;
|
|
1085
|
+
const generation = this.generation;
|
|
1086
|
+
this.patch({ error: null });
|
|
1087
|
+
try {
|
|
1088
|
+
await client.deleteMessage(messageId);
|
|
1089
|
+
} catch (cause) {
|
|
1090
|
+
if (!this.alive(generation)) return false;
|
|
1091
|
+
if (!isMessageMissing(cause)) {
|
|
1092
|
+
this.fail(cause, generation);
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (!this.alive(generation)) return false;
|
|
1097
|
+
this.removeMessage(messageId);
|
|
1098
|
+
return true;
|
|
1099
|
+
};
|
|
913
1100
|
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId, this.state.readPositionByUserId);
|
|
914
1101
|
};
|
|
915
1102
|
|
|
@@ -963,11 +1150,18 @@ function useConversation(options) {
|
|
|
963
1150
|
hasLoaded: field("hasLoaded"),
|
|
964
1151
|
error: field("error"),
|
|
965
1152
|
currentUserId: field("currentUserId"),
|
|
1153
|
+
editingMessage: field("editingMessage"),
|
|
1154
|
+
canEditMessages: field("canEditMessages"),
|
|
1155
|
+
canDeleteMessages: field("canDeleteMessages"),
|
|
966
1156
|
readerIdsFor: (message) => store.readerIdsFor(message),
|
|
967
1157
|
loadInitial: () => store.loadInitial(),
|
|
968
1158
|
refresh: () => store.refresh(),
|
|
969
1159
|
loadOlderMessages: () => store.loadOlderMessages(),
|
|
970
1160
|
sendMessage: (input) => store.sendMessage(input),
|
|
1161
|
+
startEditing: (messageId) => store.startEditing(messageId),
|
|
1162
|
+
cancelEditing: () => store.cancelEditing(),
|
|
1163
|
+
saveEdit: (text) => store.saveEdit(text),
|
|
1164
|
+
deleteMessage: (messageId) => store.deleteMessage(messageId),
|
|
971
1165
|
markRead: () => store.markRead(),
|
|
972
1166
|
updateTyping: (isTyping) => store.updateTyping(isTyping),
|
|
973
1167
|
setVisible: (value) => {
|
|
@@ -979,6 +1173,7 @@ function useConversation(options) {
|
|
|
979
1173
|
}
|
|
980
1174
|
|
|
981
1175
|
// src/components/message-list.ts
|
|
1176
|
+
var import_sdk3 = require("@convokitapp/sdk");
|
|
982
1177
|
var import_vue4 = require("@lucide/vue");
|
|
983
1178
|
var import_vue5 = require("vue");
|
|
984
1179
|
var appearanceProps = {
|
|
@@ -1033,6 +1228,10 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
1033
1228
|
isLoadingOlder: { type: Boolean, default: false },
|
|
1034
1229
|
error: { type: null, required: false },
|
|
1035
1230
|
onAttachmentClick: { type: Function, default: void 0 },
|
|
1231
|
+
onEditMessage: { type: Function, default: void 0 },
|
|
1232
|
+
onDeleteMessage: { type: Function, default: void 0 },
|
|
1233
|
+
canEditMessage: { type: Function, default: void 0 },
|
|
1234
|
+
confirmDelete: { type: Function, default: void 0 },
|
|
1036
1235
|
scrollElement: { type: Object, default: void 0 },
|
|
1037
1236
|
paginationThreshold: { type: Number, default: 240 },
|
|
1038
1237
|
reverse: { type: Boolean, default: true },
|
|
@@ -1040,9 +1239,10 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
1040
1239
|
formatTime: { type: Function, default: formatMessageTime },
|
|
1041
1240
|
imageLoading: { type: String, default: "lazy" }
|
|
1042
1241
|
},
|
|
1043
|
-
emits: ["load-older", "attachment-click"],
|
|
1242
|
+
emits: ["load-older", "attachment-click", "edit-message", "delete-message"],
|
|
1044
1243
|
setup(props, { attrs, emit, slots }) {
|
|
1045
1244
|
const internalElement = (0, import_vue5.ref)(null);
|
|
1245
|
+
const confirming = (0, import_vue5.ref)(null);
|
|
1046
1246
|
let requestInFlight = false;
|
|
1047
1247
|
let lastRequestedLength = null;
|
|
1048
1248
|
let previousMessageCount = 0;
|
|
@@ -1082,16 +1282,77 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
1082
1282
|
}
|
|
1083
1283
|
}
|
|
1084
1284
|
}, { flush: "post", immediate: true });
|
|
1285
|
+
const viewerRole = (0, import_vue5.computed)(() => props.conversation.membership?.role ?? props.conversation.participants.find((participant) => participant.appUserId === props.currentUserId || participant.id === props.currentUserId)?.role);
|
|
1286
|
+
const remove = async (message) => {
|
|
1287
|
+
if (props.confirmDelete && !await props.confirmDelete(message)) return false;
|
|
1288
|
+
return await props.onDeleteMessage?.(message) !== false;
|
|
1289
|
+
};
|
|
1085
1290
|
const renderMessage = (message, index) => {
|
|
1086
1291
|
const isCurrentUser = message.senderId === props.currentUserId;
|
|
1087
1292
|
const sender = participants.value.get(message.senderId);
|
|
1088
1293
|
const isPending = isConvoKitPendingMessage(message);
|
|
1089
1294
|
const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId, props.readPositionByUserId);
|
|
1090
|
-
const
|
|
1295
|
+
const isEdited = !isPending && (0, import_sdk3.isEditedMessage)(message);
|
|
1296
|
+
const eligible = !isPending && (props.canEditMessage ? props.canEditMessage(message) : isCurrentUser && viewerRole.value !== "READ");
|
|
1297
|
+
const canEdit = eligible && !!props.onEditMessage;
|
|
1298
|
+
const canDelete = eligible && !!props.onDeleteMessage;
|
|
1299
|
+
const edit = () => {
|
|
1300
|
+
props.onEditMessage?.(message);
|
|
1301
|
+
};
|
|
1302
|
+
const slotProps = {
|
|
1303
|
+
message,
|
|
1304
|
+
chronologicalIndex: index,
|
|
1305
|
+
isCurrentUser,
|
|
1306
|
+
sender,
|
|
1307
|
+
readerIds,
|
|
1308
|
+
isEdited,
|
|
1309
|
+
canEdit,
|
|
1310
|
+
canDelete,
|
|
1311
|
+
...canEdit ? { edit } : {},
|
|
1312
|
+
...canDelete ? { remove: () => remove(message) } : {}
|
|
1313
|
+
};
|
|
1091
1314
|
const custom = slots.message?.(slotProps);
|
|
1092
1315
|
if (custom) return (0, import_vue5.h)("div", { key: message.id, role: "listitem" }, custom);
|
|
1093
1316
|
const currentAppearance = appearance();
|
|
1094
1317
|
const messagePart = isCurrentUser ? "outgoingMessage" : "incomingMessage";
|
|
1318
|
+
const iconButton = (label, onClick, icon) => (0, import_vue5.h)("button", {
|
|
1319
|
+
type: "button",
|
|
1320
|
+
"aria-label": label,
|
|
1321
|
+
onClick,
|
|
1322
|
+
class: partClass("button", currentAppearance, "ckui-icon-button"),
|
|
1323
|
+
style: partStyle("button", currentAppearance)
|
|
1324
|
+
}, [icon]);
|
|
1325
|
+
const actions = canEdit || canDelete ? (0, import_vue5.h)("div", { class: "ckui-message-actions" }, [
|
|
1326
|
+
...canEdit ? [iconButton("Edit message", edit, (0, import_vue5.h)(import_vue4.Pencil, { size: 16, "aria-hidden": "true" }))] : [],
|
|
1327
|
+
...canDelete ? [iconButton("Delete message", () => {
|
|
1328
|
+
if (props.confirmDelete) void remove(message);
|
|
1329
|
+
else confirming.value = message.id;
|
|
1330
|
+
}, (0, import_vue5.h)(import_vue4.Trash2, { size: 16, "aria-hidden": "true" }))] : []
|
|
1331
|
+
]) : null;
|
|
1332
|
+
const confirm = canDelete && confirming.value === message.id ? (0, import_vue5.h)("div", {
|
|
1333
|
+
class: "ckui-message-confirm",
|
|
1334
|
+
role: "group",
|
|
1335
|
+
"aria-label": "Delete this message?"
|
|
1336
|
+
}, [
|
|
1337
|
+
(0, import_vue5.h)("span", "Delete this message?"),
|
|
1338
|
+
(0, import_vue5.h)("button", {
|
|
1339
|
+
type: "button",
|
|
1340
|
+
class: "ckui-link-button",
|
|
1341
|
+
"aria-label": "Confirm delete",
|
|
1342
|
+
onClick: () => {
|
|
1343
|
+
confirming.value = null;
|
|
1344
|
+
void props.onDeleteMessage?.(message);
|
|
1345
|
+
}
|
|
1346
|
+
}, "Delete"),
|
|
1347
|
+
(0, import_vue5.h)("button", {
|
|
1348
|
+
type: "button",
|
|
1349
|
+
class: "ckui-link-button",
|
|
1350
|
+
"aria-label": "Cancel delete",
|
|
1351
|
+
onClick: () => {
|
|
1352
|
+
confirming.value = null;
|
|
1353
|
+
}
|
|
1354
|
+
}, "Cancel")
|
|
1355
|
+
]) : null;
|
|
1095
1356
|
const mediaNodes = message.media.map((media, mediaIndex) => {
|
|
1096
1357
|
const open = props.onAttachmentClick ? () => {
|
|
1097
1358
|
props.onAttachmentClick?.(media, message);
|
|
@@ -1115,15 +1376,19 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
1115
1376
|
style: [props.styles?.message, props.styles?.[messagePart]],
|
|
1116
1377
|
"data-message-id": message.id
|
|
1117
1378
|
}, [
|
|
1379
|
+
// Spread, not null: an absent action/label/prompt must not leave a comment node (0.7 markup stays byte-identical).
|
|
1380
|
+
...actions ? [actions] : [],
|
|
1118
1381
|
(0, import_vue5.h)("div", { class: "ckui-message-bubble" }, [
|
|
1119
1382
|
!isCurrentUser ? (0, import_vue5.h)("strong", { class: "ckui-message-sender" }, sender?.name || message.senderId) : null,
|
|
1120
1383
|
message.text ? (0, import_vue5.h)("div", { class: "ckui-message-text" }, message.text) : null,
|
|
1121
1384
|
...mediaNodes,
|
|
1122
1385
|
(0, import_vue5.h)("span", { class: "ckui-message-time" }, [
|
|
1123
1386
|
isPending ? "Sending\u2026" : props.formatTime(message.createdAt),
|
|
1387
|
+
...isEdited ? [(0, import_vue5.h)("span", { class: "ckui-message-edited", "aria-label": "Edited" }, "Edited")] : [],
|
|
1124
1388
|
isCurrentUser && !isPending ? readerIds.size > 0 ? (0, import_vue5.h)(import_vue4.CheckCheck, { size: 14, "aria-label": "Read" }) : (0, import_vue5.h)(import_vue4.Check, { size: 14, "aria-label": "Sent" }) : null
|
|
1125
1389
|
])
|
|
1126
1390
|
]),
|
|
1391
|
+
...confirm ? [confirm] : [],
|
|
1127
1392
|
isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? (0, import_vue5.h)("div", {
|
|
1128
1393
|
class: partClass("receipt", currentAppearance, "ckui-read-receipt"),
|
|
1129
1394
|
style: partStyle("receipt", currentAppearance)
|
|
@@ -1212,6 +1477,13 @@ var viewProps = {
|
|
|
1212
1477
|
onTypingChange: { type: Function, default: void 0 },
|
|
1213
1478
|
onAddAttachment: { type: Function, default: void 0 },
|
|
1214
1479
|
onAttachmentClick: { type: Function, default: void 0 },
|
|
1480
|
+
editingMessage: { type: Object, default: null },
|
|
1481
|
+
onEditMessage: { type: Function, default: void 0 },
|
|
1482
|
+
onSaveEdit: { type: Function, default: void 0 },
|
|
1483
|
+
onCancelEdit: { type: Function, default: void 0 },
|
|
1484
|
+
onDeleteMessage: { type: Function, default: void 0 },
|
|
1485
|
+
canEditMessage: { type: Function, default: void 0 },
|
|
1486
|
+
confirmDelete: { type: Function, default: void 0 },
|
|
1215
1487
|
isInitialLoading: { type: Boolean, default: false },
|
|
1216
1488
|
isLoadingOlder: { type: Boolean, default: false },
|
|
1217
1489
|
isSending: { type: Boolean, default: false },
|
|
@@ -1231,6 +1503,9 @@ var viewProps = {
|
|
|
1231
1503
|
defaultDraft: { type: String, default: "" },
|
|
1232
1504
|
onDraftChange: { type: Function, default: void 0 }
|
|
1233
1505
|
};
|
|
1506
|
+
function editingSummary(message) {
|
|
1507
|
+
return message.text?.trim() || (message.media.length === 1 ? "1 attachment" : `${message.media.length} attachments`);
|
|
1508
|
+
}
|
|
1234
1509
|
function typingLabel(userIds, displayNameForUser) {
|
|
1235
1510
|
const names = [...userIds].map(displayNameForUser);
|
|
1236
1511
|
if (names.length === 0) return "";
|
|
@@ -1250,7 +1525,11 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1250
1525
|
"load-older",
|
|
1251
1526
|
"add-attachment",
|
|
1252
1527
|
"attachment-click",
|
|
1253
|
-
"update:modelValue"
|
|
1528
|
+
"update:modelValue",
|
|
1529
|
+
"edit-message",
|
|
1530
|
+
"save-edit",
|
|
1531
|
+
"cancel-edit",
|
|
1532
|
+
"delete-message"
|
|
1254
1533
|
],
|
|
1255
1534
|
setup(props, { attrs, emit, slots }) {
|
|
1256
1535
|
const internalDraft = (0, import_vue7.ref)(props.defaultDraft);
|
|
@@ -1263,15 +1542,57 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1263
1542
|
});
|
|
1264
1543
|
const draft = () => props.modelValue ?? internalDraft.value;
|
|
1265
1544
|
let latestDraft = draft();
|
|
1266
|
-
|
|
1545
|
+
let stash;
|
|
1546
|
+
let saving = false;
|
|
1547
|
+
const setDraft = (value, typing = true) => {
|
|
1267
1548
|
latestDraft = value;
|
|
1268
1549
|
if (props.modelValue === void 0) internalDraft.value = value;
|
|
1269
1550
|
props.onDraftChange?.(value);
|
|
1270
1551
|
emit("update:modelValue", value);
|
|
1271
|
-
|
|
1272
|
-
|
|
1552
|
+
if (typing) void props.onTypingChange?.(value.trim().length > 0);
|
|
1553
|
+
};
|
|
1554
|
+
const enterEdit = (message) => {
|
|
1555
|
+
if (stash === void 0) stash = draft();
|
|
1556
|
+
setDraft(message.text ?? "", false);
|
|
1557
|
+
};
|
|
1558
|
+
const leaveEdit = (message, restore) => {
|
|
1559
|
+
if (stash === void 0) return;
|
|
1560
|
+
const saved = stash;
|
|
1561
|
+
stash = void 0;
|
|
1562
|
+
const current = draft();
|
|
1563
|
+
if (restore === "always" || current.trim() === "" || current === (message.text ?? "")) setDraft(saved);
|
|
1273
1564
|
};
|
|
1565
|
+
(0, import_vue7.watch)(() => props.editingMessage, (next, previous) => {
|
|
1566
|
+
if (next && (!previous || previous.id !== next.id)) enterEdit(next);
|
|
1567
|
+
else if (!next && previous && !saving) leaveEdit(previous, "unchanged");
|
|
1568
|
+
}, { immediate: true });
|
|
1569
|
+
const cancelEdit = () => {
|
|
1570
|
+
const editing = props.editingMessage;
|
|
1571
|
+
if (!editing) return;
|
|
1572
|
+
leaveEdit(editing, "always");
|
|
1573
|
+
props.onCancelEdit?.();
|
|
1574
|
+
};
|
|
1575
|
+
const canSave = (editing) => draft().trim().length > 0 || editing.media.length > 0;
|
|
1274
1576
|
const submit = async () => {
|
|
1577
|
+
const editing = props.editingMessage;
|
|
1578
|
+
if (editing) {
|
|
1579
|
+
const text2 = draft().trim();
|
|
1580
|
+
if (!canSave(editing) || props.isSending || submitting.value) return;
|
|
1581
|
+
submitting.value = true;
|
|
1582
|
+
saving = true;
|
|
1583
|
+
try {
|
|
1584
|
+
const saved = await props.onSaveEdit?.(editing, text2);
|
|
1585
|
+
if (saved === false) {
|
|
1586
|
+
if (props.editingMessage === null) leaveEdit(editing, "unchanged");
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
leaveEdit(editing, "always");
|
|
1590
|
+
} finally {
|
|
1591
|
+
saving = false;
|
|
1592
|
+
submitting.value = false;
|
|
1593
|
+
}
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1275
1596
|
const originalDraft = draft();
|
|
1276
1597
|
const text = originalDraft.trim();
|
|
1277
1598
|
if (!text || props.isSending || submitting.value) return;
|
|
@@ -1340,6 +1661,7 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1340
1661
|
}, typingLabel(props.typingUserIds, nameForUser));
|
|
1341
1662
|
};
|
|
1342
1663
|
const renderComposer = () => {
|
|
1664
|
+
const editing = props.editingMessage;
|
|
1343
1665
|
const slotProps = {
|
|
1344
1666
|
value: draft(),
|
|
1345
1667
|
setValue: setDraft,
|
|
@@ -1347,16 +1669,27 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1347
1669
|
send: () => {
|
|
1348
1670
|
void submit();
|
|
1349
1671
|
},
|
|
1350
|
-
...props.onAddAttachment ? { addAttachment } : {}
|
|
1672
|
+
...props.onAddAttachment ? { addAttachment } : {},
|
|
1673
|
+
...editing ? { editing, cancelEdit } : {}
|
|
1351
1674
|
};
|
|
1675
|
+
const busy = props.isSending || submitting.value;
|
|
1352
1676
|
return slots.composer?.(slotProps) ?? (0, import_vue7.h)("form", {
|
|
1353
|
-
class: partClass("composer", appearance(), "ckui-composer"),
|
|
1677
|
+
class: cx(partClass("composer", appearance(), "ckui-composer"), editing && !props.unstyled && "ckui-composer--editing"),
|
|
1354
1678
|
style: partStyle("composer", appearance()),
|
|
1355
1679
|
onSubmit: (event) => {
|
|
1356
1680
|
event.preventDefault();
|
|
1357
1681
|
void submit();
|
|
1358
1682
|
}
|
|
1359
1683
|
}, [
|
|
1684
|
+
// Spread, not null: outside edit mode the composer markup stays byte-identical to 0.7.
|
|
1685
|
+
...editing ? [(0, import_vue7.h)("div", { class: "ckui-composer__editing", role: "status" }, [
|
|
1686
|
+
(0, import_vue7.h)(import_vue6.Pencil, { size: 14, "aria-hidden": "true" }),
|
|
1687
|
+
(0, import_vue7.h)("span", { class: "ckui-composer__editing-body" }, [
|
|
1688
|
+
(0, import_vue7.h)("strong", "Editing message"),
|
|
1689
|
+
(0, import_vue7.h)("span", editingSummary(editing))
|
|
1690
|
+
]),
|
|
1691
|
+
(0, import_vue7.h)("button", { type: "button", class: "ckui-link-button", "aria-label": "Cancel editing", onClick: cancelEdit }, "Cancel")
|
|
1692
|
+
])] : [],
|
|
1360
1693
|
props.onAddAttachment ? (0, import_vue7.h)("button", {
|
|
1361
1694
|
type: "button",
|
|
1362
1695
|
"aria-label": "Add attachment",
|
|
@@ -1385,15 +1718,19 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1385
1718
|
event.preventDefault();
|
|
1386
1719
|
void submit();
|
|
1387
1720
|
}
|
|
1721
|
+
if (event.key === "Escape" && props.editingMessage) {
|
|
1722
|
+
event.preventDefault();
|
|
1723
|
+
cancelEdit();
|
|
1724
|
+
}
|
|
1388
1725
|
}
|
|
1389
1726
|
}),
|
|
1390
1727
|
(0, import_vue7.h)("button", {
|
|
1391
1728
|
type: "submit",
|
|
1392
|
-
"aria-label": "Send message",
|
|
1393
|
-
disabled: !draft().trim() ||
|
|
1729
|
+
"aria-label": editing ? "Save message" : "Send message",
|
|
1730
|
+
disabled: (editing ? !canSave(editing) : !draft().trim()) || busy,
|
|
1394
1731
|
class: partClass("button", appearance(), "ckui-send-button"),
|
|
1395
1732
|
style: partStyle("button", appearance())
|
|
1396
|
-
}, [
|
|
1733
|
+
}, [busy ? (0, import_vue7.h)(import_vue6.LoaderCircle, { class: "ckui-spin", size: 18, "aria-hidden": "true" }) : (0, import_vue7.h)(editing ? import_vue6.Check : import_vue6.Send, { size: 18, "aria-hidden": "true" })])
|
|
1397
1734
|
]);
|
|
1398
1735
|
};
|
|
1399
1736
|
return () => {
|
|
@@ -1442,6 +1779,12 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1442
1779
|
...props.onAttachmentClick ? { onAttachmentClick: (media, message) => {
|
|
1443
1780
|
props.onAttachmentClick?.(media, message);
|
|
1444
1781
|
} } : {},
|
|
1782
|
+
...props.onEditMessage ? { onEditMessage: (message) => {
|
|
1783
|
+
props.onEditMessage?.(message);
|
|
1784
|
+
} } : {},
|
|
1785
|
+
...props.onDeleteMessage ? { onDeleteMessage: (message) => props.onDeleteMessage?.(message) } : {},
|
|
1786
|
+
...props.canEditMessage ? { canEditMessage: props.canEditMessage } : {},
|
|
1787
|
+
...props.confirmDelete ? { confirmDelete: props.confirmDelete } : {},
|
|
1445
1788
|
reverse: props.reverseMessages,
|
|
1446
1789
|
stickToBottom: props.stickToBottom,
|
|
1447
1790
|
paginationThreshold: props.paginationThreshold,
|
|
@@ -1472,6 +1815,11 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1472
1815
|
messages: { type: Array, default: () => [] },
|
|
1473
1816
|
currentUserId: { type: String, default: "" },
|
|
1474
1817
|
onSendMessage: { type: Function, default: void 0 },
|
|
1818
|
+
editingMessage: { type: Object, default: void 0 },
|
|
1819
|
+
onEditMessage: { type: Function, default: void 0 },
|
|
1820
|
+
onSaveEdit: { type: Function, default: void 0 },
|
|
1821
|
+
onCancelEdit: { type: Function, default: void 0 },
|
|
1822
|
+
onDeleteMessage: { type: Function, default: void 0 },
|
|
1475
1823
|
client: { type: Object, required: true },
|
|
1476
1824
|
conversationId: { type: String, required: true },
|
|
1477
1825
|
messagePageSize: { type: Number, default: 30 },
|
|
@@ -1481,7 +1829,21 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1481
1829
|
autoLoad: { type: Boolean, default: true },
|
|
1482
1830
|
onControllerChange: { type: Function, default: void 0 }
|
|
1483
1831
|
},
|
|
1484
|
-
emits: [
|
|
1832
|
+
emits: [
|
|
1833
|
+
"controller-change",
|
|
1834
|
+
"send-message",
|
|
1835
|
+
"typing-change",
|
|
1836
|
+
"back",
|
|
1837
|
+
"refresh",
|
|
1838
|
+
"load-older",
|
|
1839
|
+
"add-attachment",
|
|
1840
|
+
"attachment-click",
|
|
1841
|
+
"update:modelValue",
|
|
1842
|
+
"edit-message",
|
|
1843
|
+
"save-edit",
|
|
1844
|
+
"cancel-edit",
|
|
1845
|
+
"delete-message"
|
|
1846
|
+
],
|
|
1485
1847
|
setup(props, { attrs, emit, expose, slots }) {
|
|
1486
1848
|
const controller = useConversation({
|
|
1487
1849
|
client: () => props.client,
|
|
@@ -1545,6 +1907,11 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1545
1907
|
isSending: _isSending,
|
|
1546
1908
|
hasOlderMessages: _hasOlderMessages,
|
|
1547
1909
|
error: _error,
|
|
1910
|
+
editingMessage: _editingMessage,
|
|
1911
|
+
onEditMessage: _onEditMessage,
|
|
1912
|
+
onSaveEdit: _onSaveEdit,
|
|
1913
|
+
onCancelEdit: _onCancelEdit,
|
|
1914
|
+
onDeleteMessage: _onDeleteMessage,
|
|
1548
1915
|
...forwarded
|
|
1549
1916
|
} = props;
|
|
1550
1917
|
return (0, import_vue7.h)(ConversationView, {
|
|
@@ -1568,6 +1935,28 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1568
1935
|
isSending: controller.isSending.value,
|
|
1569
1936
|
hasOlderMessages: controller.hasOlderMessages.value,
|
|
1570
1937
|
...controller.error.value == null ? {} : { error: controller.error.value },
|
|
1938
|
+
// Edit mode is the store's; the actions render only while the adapter supports them (0.8.0).
|
|
1939
|
+
editingMessage: controller.editingMessage.value,
|
|
1940
|
+
...controller.canEditMessages.value ? {
|
|
1941
|
+
onEditMessage: (message) => {
|
|
1942
|
+
emit("edit-message", message);
|
|
1943
|
+
controller.startEditing(message.id);
|
|
1944
|
+
},
|
|
1945
|
+
onSaveEdit: async (message, text) => {
|
|
1946
|
+
emit("save-edit", message, text);
|
|
1947
|
+
return controller.saveEdit(text);
|
|
1948
|
+
},
|
|
1949
|
+
onCancelEdit: () => {
|
|
1950
|
+
emit("cancel-edit");
|
|
1951
|
+
controller.cancelEditing();
|
|
1952
|
+
}
|
|
1953
|
+
} : {},
|
|
1954
|
+
...controller.canDeleteMessages.value ? {
|
|
1955
|
+
onDeleteMessage: async (message) => {
|
|
1956
|
+
emit("delete-message", message);
|
|
1957
|
+
return controller.deleteMessage(message.id);
|
|
1958
|
+
}
|
|
1959
|
+
} : {},
|
|
1571
1960
|
"onUpdate:modelValue": (value) => emit("update:modelValue", value),
|
|
1572
1961
|
...props.onBack ? { onBack: () => {
|
|
1573
1962
|
props.onBack?.();
|