@ai-matrx/messaging 0.15.82 → 0.16.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 +46 -1
- package/dist/index.cjs +171 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -2
- package/dist/index.d.ts +71 -2
- package/dist/index.js +171 -9
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +206 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +73 -2
- package/dist/react.d.ts +73 -2
- package/dist/react.js +206 -10
- package/dist/react.js.map +1 -1
- package/dist/styles.css +15 -0
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -97,6 +97,7 @@ __export(react_exports, {
|
|
|
97
97
|
groupMessages: () => groupMessages,
|
|
98
98
|
inboxTopic: () => inboxTopic,
|
|
99
99
|
invalidResponse: () => invalidResponse,
|
|
100
|
+
isLapsed: () => isLapsed,
|
|
100
101
|
isSameDay: () => isSameDay,
|
|
101
102
|
messagingClientId: () => messagingClientId,
|
|
102
103
|
normalizeMessagingError: () => normalizeMessagingError,
|
|
@@ -108,6 +109,7 @@ __export(react_exports, {
|
|
|
108
109
|
projectParticipantRole: () => projectParticipantRole,
|
|
109
110
|
projectUserSummary: () => projectUserSummary,
|
|
110
111
|
resolveActor: () => resolveActor,
|
|
112
|
+
softExpiryTime: () => softExpiryTime,
|
|
111
113
|
splitText: () => splitText,
|
|
112
114
|
summarizeText: () => summarizeText,
|
|
113
115
|
toMessagingArchiveFilter: () => toMessagingArchiveFilter,
|
|
@@ -911,7 +913,8 @@ function projectBroadcastMessage(value) {
|
|
|
911
913
|
action: projectMessageAction(record["action"]),
|
|
912
914
|
attachments: projectAttachmentList(record["attachments"]),
|
|
913
915
|
references: projectReferenceList(record["references"]),
|
|
914
|
-
metadata
|
|
916
|
+
metadata,
|
|
917
|
+
softExpiresAt: str(record, "softExpiresAt")
|
|
915
918
|
};
|
|
916
919
|
}
|
|
917
920
|
function projectMessage(row, fallbackOrganizationId) {
|
|
@@ -941,7 +944,8 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
941
944
|
action,
|
|
942
945
|
attachments: projectAttachments(metadata),
|
|
943
946
|
references: projectReferences(metadata),
|
|
944
|
-
metadata
|
|
947
|
+
metadata,
|
|
948
|
+
softExpiresAt: str(row, "soft_expires_at")
|
|
945
949
|
};
|
|
946
950
|
}
|
|
947
951
|
function projectParticipantRole(value) {
|
|
@@ -994,6 +998,18 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
994
998
|
};
|
|
995
999
|
}
|
|
996
1000
|
|
|
1001
|
+
// src/core/lapse.ts
|
|
1002
|
+
function softExpiryTime(message) {
|
|
1003
|
+
if (message.softExpiresAt === null) return null;
|
|
1004
|
+
const parsed = Date.parse(message.softExpiresAt);
|
|
1005
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1006
|
+
}
|
|
1007
|
+
function isLapsed(message, now = Date.now()) {
|
|
1008
|
+
const at = softExpiryTime(message);
|
|
1009
|
+
if (at === null) return false;
|
|
1010
|
+
return at <= (typeof now === "number" ? now : now.getTime());
|
|
1011
|
+
}
|
|
1012
|
+
|
|
997
1013
|
// src/core/types.ts
|
|
998
1014
|
var asConversationId = (value) => value;
|
|
999
1015
|
var asMessageId = (value) => value;
|
|
@@ -1031,7 +1047,8 @@ function insertOrdered(messages, message) {
|
|
|
1031
1047
|
next.splice(index, 0, message);
|
|
1032
1048
|
return next;
|
|
1033
1049
|
}
|
|
1034
|
-
function createMessagingStore() {
|
|
1050
|
+
function createMessagingStore(options = {}) {
|
|
1051
|
+
const now = options.now ?? Date.now;
|
|
1035
1052
|
let conversations = [];
|
|
1036
1053
|
let conversationSummaries = /* @__PURE__ */ new Map();
|
|
1037
1054
|
let hasMoreConversations = false;
|
|
@@ -1268,7 +1285,7 @@ function createMessagingStore() {
|
|
|
1268
1285
|
if (existing === void 0) return;
|
|
1269
1286
|
if (message.deliveryState === "sending" || message.deletedAt !== null) return;
|
|
1270
1287
|
if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
|
|
1271
|
-
const shouldCount = args.incrementUnread === true;
|
|
1288
|
+
const shouldCount = args.incrementUnread === true && !isLapsed(message, now());
|
|
1272
1289
|
replaceOneSummary(message.conversationId, (item) => ({
|
|
1273
1290
|
...item,
|
|
1274
1291
|
lastMessageContent: message.content,
|
|
@@ -1331,14 +1348,25 @@ function optimisticMessage(args) {
|
|
|
1331
1348
|
action: args.action ?? null,
|
|
1332
1349
|
attachments: args.attachments ?? [],
|
|
1333
1350
|
references: args.references ?? [],
|
|
1334
|
-
metadata: {}
|
|
1351
|
+
metadata: {},
|
|
1352
|
+
softExpiresAt: args.softExpiresAt ?? null
|
|
1335
1353
|
};
|
|
1336
1354
|
}
|
|
1337
1355
|
|
|
1338
1356
|
// src/core/engine.ts
|
|
1357
|
+
var SOFT_EXPIRY_GRACE_MS = 2e3;
|
|
1358
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1339
1359
|
function createMessagingEngine(options) {
|
|
1340
1360
|
const { repository, manager, identity } = options;
|
|
1341
|
-
const
|
|
1361
|
+
const browserNow = options.now ?? Date.now;
|
|
1362
|
+
let serverClockOffsetMs = 0;
|
|
1363
|
+
const now = () => browserNow() + serverClockOffsetMs;
|
|
1364
|
+
function learnServerClock(serverNow) {
|
|
1365
|
+
if (serverNow === null) return;
|
|
1366
|
+
const server = Date.parse(serverNow);
|
|
1367
|
+
if (Number.isFinite(server)) serverClockOffsetMs = server - browserNow();
|
|
1368
|
+
}
|
|
1369
|
+
const store = createMessagingStore({ now });
|
|
1342
1370
|
const conversationPageSize = options.conversationPageSize ?? 30;
|
|
1343
1371
|
const messagePageSize = options.messagePageSize ?? 50;
|
|
1344
1372
|
const openChannels = /* @__PURE__ */ new Map();
|
|
@@ -1352,6 +1380,8 @@ function createMessagingEngine(options) {
|
|
|
1352
1380
|
let inboxChannel = null;
|
|
1353
1381
|
let conversationCursor = null;
|
|
1354
1382
|
let disposed = false;
|
|
1383
|
+
const pendingExpiry = /* @__PURE__ */ new Map();
|
|
1384
|
+
let expiryTimer = null;
|
|
1355
1385
|
function report(event) {
|
|
1356
1386
|
options.onDiagnostic?.(event);
|
|
1357
1387
|
}
|
|
@@ -1394,6 +1424,72 @@ function createMessagingEngine(options) {
|
|
|
1394
1424
|
}).catch(() => void 0);
|
|
1395
1425
|
return current;
|
|
1396
1426
|
}
|
|
1427
|
+
function scheduleExpiryTimer() {
|
|
1428
|
+
if (expiryTimer !== null) {
|
|
1429
|
+
clearTimeout(expiryTimer);
|
|
1430
|
+
expiryTimer = null;
|
|
1431
|
+
}
|
|
1432
|
+
if (disposed || pendingExpiry.size === 0) return;
|
|
1433
|
+
const soonest = Math.min(...pendingExpiry.values());
|
|
1434
|
+
const delay = Math.min(
|
|
1435
|
+
MAX_TIMER_DELAY_MS,
|
|
1436
|
+
Math.max(0, soonest + SOFT_EXPIRY_GRACE_MS - now())
|
|
1437
|
+
);
|
|
1438
|
+
expiryTimer = setTimeout(() => {
|
|
1439
|
+
expiryTimer = null;
|
|
1440
|
+
void fireSoftExpiry();
|
|
1441
|
+
}, delay);
|
|
1442
|
+
}
|
|
1443
|
+
function noteSoftExpiry(id, at) {
|
|
1444
|
+
if (at === null) return;
|
|
1445
|
+
if (at <= now() && (store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
const held = pendingExpiry.get(id);
|
|
1449
|
+
if (held !== void 0 && held <= at) return;
|
|
1450
|
+
pendingExpiry.set(id, at);
|
|
1451
|
+
scheduleExpiryTimer();
|
|
1452
|
+
}
|
|
1453
|
+
async function refreshPendingExpiries(ids) {
|
|
1454
|
+
const summaries = store.snapshot().conversationSummaries;
|
|
1455
|
+
const counted = ids.filter((id) => (summaries.get(id)?.unreadCount ?? 0) > 0);
|
|
1456
|
+
ids.forEach((id) => pendingExpiry.delete(id));
|
|
1457
|
+
if (counted.length > 0) {
|
|
1458
|
+
const pending = await repository.pendingSoftExpiries(counted);
|
|
1459
|
+
if (disposed) return;
|
|
1460
|
+
learnServerClock(pending.serverNow);
|
|
1461
|
+
pending.byConversation.forEach(
|
|
1462
|
+
(at, id) => noteSoftExpiry(id, softExpiryTime({ softExpiresAt: at }))
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
scheduleExpiryTimer();
|
|
1466
|
+
}
|
|
1467
|
+
async function fireSoftExpiry() {
|
|
1468
|
+
if (disposed) return;
|
|
1469
|
+
const at = now();
|
|
1470
|
+
const due = [...pendingExpiry.entries()].filter(([, expiry]) => expiry <= at).map(([id]) => id);
|
|
1471
|
+
due.forEach((id) => pendingExpiry.delete(id));
|
|
1472
|
+
await Promise.all(
|
|
1473
|
+
due.map(async (id) => {
|
|
1474
|
+
if ((store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) return;
|
|
1475
|
+
try {
|
|
1476
|
+
const count = await repository.unreadCount(id);
|
|
1477
|
+
if (!disposed) store.setUnreadCount(id, count);
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
reportError(error, "softExpiryUnread", { conversationId: id });
|
|
1480
|
+
}
|
|
1481
|
+
})
|
|
1482
|
+
);
|
|
1483
|
+
if (disposed) return;
|
|
1484
|
+
await refreshPendingExpiries(due).catch(
|
|
1485
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
function watchSoftExpiries(ids) {
|
|
1489
|
+
void refreshPendingExpiries(ids).catch(
|
|
1490
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1397
1493
|
const outbox = createOutbox({
|
|
1398
1494
|
...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
|
|
1399
1495
|
send: (draft, clientMessageId) => repository.insertMessage(draft, clientMessageId),
|
|
@@ -1420,7 +1516,8 @@ function createMessagingEngine(options) {
|
|
|
1420
1516
|
...entry.draft.replyToId !== void 0 ? { replyToId: entry.draft.replyToId } : {},
|
|
1421
1517
|
...entry.draft.action !== void 0 ? { action: entry.draft.action } : {},
|
|
1422
1518
|
...entry.draft.attachments !== void 0 ? { attachments: [...entry.draft.attachments] } : {},
|
|
1423
|
-
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {}
|
|
1519
|
+
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {},
|
|
1520
|
+
...entry.draft.softExpiresAt !== void 0 ? { softExpiresAt: entry.draft.softExpiresAt } : {}
|
|
1424
1521
|
})
|
|
1425
1522
|
);
|
|
1426
1523
|
});
|
|
@@ -1449,6 +1546,7 @@ function createMessagingEngine(options) {
|
|
|
1449
1546
|
if (disposed) return;
|
|
1450
1547
|
conversationCursor = page.nextCursor;
|
|
1451
1548
|
store.setConversations(page.items, page.hasMore);
|
|
1549
|
+
watchSoftExpiries([...store.snapshot().conversationSummaries.keys()]);
|
|
1452
1550
|
}
|
|
1453
1551
|
async function reloadArchivedCount() {
|
|
1454
1552
|
if (disposed) return;
|
|
@@ -1643,6 +1741,7 @@ function createMessagingEngine(options) {
|
|
|
1643
1741
|
store.ingest(message);
|
|
1644
1742
|
const isMine = message.senderId === identity.userId;
|
|
1645
1743
|
store.applyLastMessage(message, { incrementUnread: !isMine });
|
|
1744
|
+
if (!isMine) noteSoftExpiry(message.conversationId, softExpiryTime(message));
|
|
1646
1745
|
if (!isMine && store.snapshot().activeConversationId === message.conversationId) {
|
|
1647
1746
|
void persistRead(message.conversationId).catch(() => void 0);
|
|
1648
1747
|
}
|
|
@@ -1704,6 +1803,7 @@ function createMessagingEngine(options) {
|
|
|
1704
1803
|
});
|
|
1705
1804
|
conversationCursor = page.nextCursor;
|
|
1706
1805
|
store.appendConversations(page.items, page.hasMore);
|
|
1806
|
+
watchSoftExpiries(page.items.map((item) => item.conversation.id));
|
|
1707
1807
|
} catch (error) {
|
|
1708
1808
|
reportError(error, "loadMoreConversations");
|
|
1709
1809
|
}
|
|
@@ -1723,7 +1823,10 @@ function createMessagingEngine(options) {
|
|
|
1723
1823
|
const directSummary = missingSummary ? await repository.getConversationSummary(id) : null;
|
|
1724
1824
|
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1725
1825
|
store.setConversationReadFailure(id, null);
|
|
1726
|
-
if (directSummary !== null)
|
|
1826
|
+
if (directSummary !== null) {
|
|
1827
|
+
store.setConversationSummary(directSummary);
|
|
1828
|
+
watchSoftExpiries([id]);
|
|
1829
|
+
}
|
|
1727
1830
|
await persistRead(
|
|
1728
1831
|
id,
|
|
1729
1832
|
() => openIsLive(id, generation, ownedChannel)
|
|
@@ -1818,6 +1921,9 @@ function createMessagingEngine(options) {
|
|
|
1818
1921
|
},
|
|
1819
1922
|
dispose() {
|
|
1820
1923
|
disposed = true;
|
|
1924
|
+
if (expiryTimer !== null) clearTimeout(expiryTimer);
|
|
1925
|
+
expiryTimer = null;
|
|
1926
|
+
pendingExpiry.clear();
|
|
1821
1927
|
openChannels.forEach((_handle, id) => {
|
|
1822
1928
|
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1823
1929
|
});
|
|
@@ -2036,10 +2142,13 @@ var RPCS = {
|
|
|
2036
2142
|
/** Conversation list + participants + last message + unread, keyset paged. */
|
|
2037
2143
|
conversationsWithDetails: "get_dm_conversations_with_details",
|
|
2038
2144
|
unreadCount: "get_dm_unread_count",
|
|
2145
|
+
/** The soonest future soft expiry per conversation, plus the server's clock. */
|
|
2146
|
+
pendingSoftExpiries: "get_dm_pending_soft_expiries",
|
|
2039
2147
|
userInfo: "get_dm_user_info",
|
|
2040
2148
|
isParticipant: "is_dm_participant"
|
|
2041
2149
|
};
|
|
2042
|
-
var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
|
|
2150
|
+
var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata,soft_expires_at";
|
|
2151
|
+
var PENDING_SOFT_EXPIRY_CHUNK = 200;
|
|
2043
2152
|
var PARTICIPANT_COLUMNS = "conversation_id,user_id,role,joined_at,last_read_at,is_muted,is_archived";
|
|
2044
2153
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
2045
2154
|
function optionalOrg(organizationId) {
|
|
@@ -2460,6 +2569,9 @@ function createMessagingRepository(options) {
|
|
|
2460
2569
|
// confirmed row instead of showing the message twice.
|
|
2461
2570
|
client_message_id: clientMessageId,
|
|
2462
2571
|
action_data: draft.action ?? null,
|
|
2572
|
+
// SOFT EXPIRY: written only when the sender sets one. Lapsed is
|
|
2573
|
+
// derived from it live (`isLapsed`), never stamped.
|
|
2574
|
+
...draft.softExpiresAt !== void 0 ? { soft_expires_at: draft.softExpiresAt } : {},
|
|
2463
2575
|
metadata: {
|
|
2464
2576
|
...draft.metadata ?? {},
|
|
2465
2577
|
...draft.attachments !== void 0 && draft.attachments.length > 0 ? { attachments: draft.attachments } : {},
|
|
@@ -2512,6 +2624,56 @@ function createMessagingRepository(options) {
|
|
|
2512
2624
|
}
|
|
2513
2625
|
return Math.floor(unread);
|
|
2514
2626
|
},
|
|
2627
|
+
async unreadCount(conversationId) {
|
|
2628
|
+
const operation = "unreadCount";
|
|
2629
|
+
const me = await actingUser(operation);
|
|
2630
|
+
const unread = await withSessionRetry(
|
|
2631
|
+
operation,
|
|
2632
|
+
() => rpc(
|
|
2633
|
+
RPCS.unreadCount,
|
|
2634
|
+
{ p_conversation_id: conversationId, p_user_id: me },
|
|
2635
|
+
operation
|
|
2636
|
+
)
|
|
2637
|
+
);
|
|
2638
|
+
if (typeof unread !== "number" || !Number.isFinite(unread) || unread < 0) {
|
|
2639
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unread)}`);
|
|
2640
|
+
}
|
|
2641
|
+
return Math.floor(unread);
|
|
2642
|
+
},
|
|
2643
|
+
async pendingSoftExpiries(conversationIds) {
|
|
2644
|
+
const operation = "pendingSoftExpiries";
|
|
2645
|
+
const unique = [...new Set(conversationIds)];
|
|
2646
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
2647
|
+
let serverNow = null;
|
|
2648
|
+
for (let start = 0; start < unique.length; start += PENDING_SOFT_EXPIRY_CHUNK) {
|
|
2649
|
+
const chunk = unique.slice(start, start + PENDING_SOFT_EXPIRY_CHUNK);
|
|
2650
|
+
const answer = await withSessionRetry(
|
|
2651
|
+
operation,
|
|
2652
|
+
() => rpc(
|
|
2653
|
+
RPCS.pendingSoftExpiries,
|
|
2654
|
+
{ p_conversation_ids: chunk },
|
|
2655
|
+
operation
|
|
2656
|
+
)
|
|
2657
|
+
);
|
|
2658
|
+
const now = answer?.["server_now"];
|
|
2659
|
+
const pending = answer?.["pending"];
|
|
2660
|
+
if (typeof now !== "string" || !Array.isArray(pending)) {
|
|
2661
|
+
throw invalidResponse(
|
|
2662
|
+
operation,
|
|
2663
|
+
`${RPCS.pendingSoftExpiries} returned ${JSON.stringify(answer)}`
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
serverNow = now;
|
|
2667
|
+
for (const row of pending) {
|
|
2668
|
+
const id = row["conversation_id"];
|
|
2669
|
+
const at = row["soft_expires_at"];
|
|
2670
|
+
if (typeof id === "string" && typeof at === "string") {
|
|
2671
|
+
byConversation.set(id, at);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return { byConversation, serverNow };
|
|
2676
|
+
},
|
|
2515
2677
|
async setConversationFlags(conversationId, flags) {
|
|
2516
2678
|
const operation = "setConversationFlags";
|
|
2517
2679
|
const me = await actingUser(operation);
|
|
@@ -3918,6 +4080,23 @@ function ConversationRow(props) {
|
|
|
3918
4080
|
}
|
|
3919
4081
|
);
|
|
3920
4082
|
}
|
|
4083
|
+
function useLapseClock(messages) {
|
|
4084
|
+
const [now, setNow] = (0, import_react5.useState)(() => Date.now());
|
|
4085
|
+
let soonest = null;
|
|
4086
|
+
for (const message of messages) {
|
|
4087
|
+
const at = softExpiryTime(message);
|
|
4088
|
+
if (at !== null && at > now && (soonest === null || at < soonest)) soonest = at;
|
|
4089
|
+
}
|
|
4090
|
+
(0, import_react5.useEffect)(() => {
|
|
4091
|
+
if (soonest === null) return;
|
|
4092
|
+
const timer = setTimeout(
|
|
4093
|
+
() => setNow(Date.now()),
|
|
4094
|
+
Math.min(2147483647, Math.max(0, soonest - Date.now()))
|
|
4095
|
+
);
|
|
4096
|
+
return () => clearTimeout(timer);
|
|
4097
|
+
}, [soonest]);
|
|
4098
|
+
return now;
|
|
4099
|
+
}
|
|
3921
4100
|
function ConversationView(props) {
|
|
3922
4101
|
const { conversationId } = props;
|
|
3923
4102
|
const host = useMessagingHost();
|
|
@@ -3930,6 +4109,7 @@ function ConversationView(props) {
|
|
|
3930
4109
|
const selfId = host?.identity.userId ?? "";
|
|
3931
4110
|
const messages = conversation.messages;
|
|
3932
4111
|
const lastId = messages.at(-1)?.id ?? null;
|
|
4112
|
+
const lapseNow = useLapseClock(messages);
|
|
3933
4113
|
(0, import_react5.useEffect)(() => {
|
|
3934
4114
|
const node = scrollRef.current;
|
|
3935
4115
|
if (node === null) return;
|
|
@@ -4061,6 +4241,7 @@ function ConversationView(props) {
|
|
|
4061
4241
|
MessageGroupView,
|
|
4062
4242
|
{
|
|
4063
4243
|
allMessages: messages,
|
|
4244
|
+
now: lapseNow,
|
|
4064
4245
|
showAuthor: conversation.summary?.conversation.type !== "direct",
|
|
4065
4246
|
messages: group.messages,
|
|
4066
4247
|
actor,
|
|
@@ -4121,6 +4302,7 @@ function MessageGroupView(props) {
|
|
|
4121
4302
|
replyTo: props.allMessages.find((parent) => parent.id === message.replyToId),
|
|
4122
4303
|
isMine: props.isMine,
|
|
4123
4304
|
isLast: index === props.messages.length - 1,
|
|
4305
|
+
now: props.now,
|
|
4124
4306
|
onRetry: props.onRetry,
|
|
4125
4307
|
onReply: props.onReply,
|
|
4126
4308
|
failedEntryId: message.clientMessageId !== null ? props.failedEntryIdByClientId.get(message.clientMessageId) ?? null : null
|
|
@@ -4140,12 +4322,15 @@ function MessageBubble(props) {
|
|
|
4140
4322
|
if (message.deletedAt !== null) {
|
|
4141
4323
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mx-msg__bubble mx-msg__bubble--deleted", children: "Message deleted" });
|
|
4142
4324
|
}
|
|
4325
|
+
const lapsed = isLapsed(message, props.now ?? Date.now());
|
|
4143
4326
|
const classes = [
|
|
4144
4327
|
"mx-msg__bubble",
|
|
4145
4328
|
isMine ? "mx-msg__bubble--mine" : "",
|
|
4146
4329
|
props.isLast ? "mx-msg__bubble--tail" : "",
|
|
4147
4330
|
message.deliveryState === "sending" ? "mx-msg__bubble--pending" : "",
|
|
4148
|
-
message.deliveryState === "failed" ? "mx-msg__bubble--failed" : ""
|
|
4331
|
+
message.deliveryState === "failed" ? "mx-msg__bubble--failed" : "",
|
|
4332
|
+
// SOFT EXPIRY: past, still fully readable and actionable — muted, never hidden.
|
|
4333
|
+
lapsed ? "mx-msg__bubble--lapsed" : ""
|
|
4149
4334
|
].filter(Boolean).join(" ");
|
|
4150
4335
|
const bubble = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
4151
4336
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: classes, title: formatMessageTime(message.createdAt), children: [
|
|
@@ -4195,6 +4380,17 @@ function MessageBubble(props) {
|
|
|
4195
4380
|
children: [
|
|
4196
4381
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "mx-msg__message-time", children: props.isLast ? formatMessageTime(message.createdAt) : null }),
|
|
4197
4382
|
message.editedAt !== null ? " \xB7 edited" : null,
|
|
4383
|
+
lapsed && message.softExpiresAt !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
4384
|
+
"span",
|
|
4385
|
+
{
|
|
4386
|
+
className: "mx-msg__lapsed",
|
|
4387
|
+
title: `Past ${formatMessageTime(message.softExpiresAt)}`,
|
|
4388
|
+
children: [
|
|
4389
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ClockIcon, {}),
|
|
4390
|
+
"Past"
|
|
4391
|
+
]
|
|
4392
|
+
}
|
|
4393
|
+
) : null,
|
|
4198
4394
|
isMine ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DeliveryTick, { state: message.deliveryState }) : null,
|
|
4199
4395
|
message.deliveryState === "failed" && props.failedEntryId != null && props.onRetry !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
4200
4396
|
"button",
|