@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.js
CHANGED
|
@@ -802,7 +802,8 @@ function projectBroadcastMessage(value) {
|
|
|
802
802
|
action: projectMessageAction(record["action"]),
|
|
803
803
|
attachments: projectAttachmentList(record["attachments"]),
|
|
804
804
|
references: projectReferenceList(record["references"]),
|
|
805
|
-
metadata
|
|
805
|
+
metadata,
|
|
806
|
+
softExpiresAt: str(record, "softExpiresAt")
|
|
806
807
|
};
|
|
807
808
|
}
|
|
808
809
|
function projectMessage(row, fallbackOrganizationId) {
|
|
@@ -832,7 +833,8 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
832
833
|
action,
|
|
833
834
|
attachments: projectAttachments(metadata),
|
|
834
835
|
references: projectReferences(metadata),
|
|
835
|
-
metadata
|
|
836
|
+
metadata,
|
|
837
|
+
softExpiresAt: str(row, "soft_expires_at")
|
|
836
838
|
};
|
|
837
839
|
}
|
|
838
840
|
function projectParticipantRole(value) {
|
|
@@ -885,6 +887,18 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
885
887
|
};
|
|
886
888
|
}
|
|
887
889
|
|
|
890
|
+
// src/core/lapse.ts
|
|
891
|
+
function softExpiryTime(message) {
|
|
892
|
+
if (message.softExpiresAt === null) return null;
|
|
893
|
+
const parsed = Date.parse(message.softExpiresAt);
|
|
894
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
895
|
+
}
|
|
896
|
+
function isLapsed(message, now = Date.now()) {
|
|
897
|
+
const at = softExpiryTime(message);
|
|
898
|
+
if (at === null) return false;
|
|
899
|
+
return at <= (typeof now === "number" ? now : now.getTime());
|
|
900
|
+
}
|
|
901
|
+
|
|
888
902
|
// src/core/types.ts
|
|
889
903
|
var asConversationId = (value) => value;
|
|
890
904
|
var asMessageId = (value) => value;
|
|
@@ -922,7 +936,8 @@ function insertOrdered(messages, message) {
|
|
|
922
936
|
next.splice(index, 0, message);
|
|
923
937
|
return next;
|
|
924
938
|
}
|
|
925
|
-
function createMessagingStore() {
|
|
939
|
+
function createMessagingStore(options = {}) {
|
|
940
|
+
const now = options.now ?? Date.now;
|
|
926
941
|
let conversations = [];
|
|
927
942
|
let conversationSummaries = /* @__PURE__ */ new Map();
|
|
928
943
|
let hasMoreConversations = false;
|
|
@@ -1159,7 +1174,7 @@ function createMessagingStore() {
|
|
|
1159
1174
|
if (existing === void 0) return;
|
|
1160
1175
|
if (message.deliveryState === "sending" || message.deletedAt !== null) return;
|
|
1161
1176
|
if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
|
|
1162
|
-
const shouldCount = args.incrementUnread === true;
|
|
1177
|
+
const shouldCount = args.incrementUnread === true && !isLapsed(message, now());
|
|
1163
1178
|
replaceOneSummary(message.conversationId, (item) => ({
|
|
1164
1179
|
...item,
|
|
1165
1180
|
lastMessageContent: message.content,
|
|
@@ -1222,14 +1237,25 @@ function optimisticMessage(args) {
|
|
|
1222
1237
|
action: args.action ?? null,
|
|
1223
1238
|
attachments: args.attachments ?? [],
|
|
1224
1239
|
references: args.references ?? [],
|
|
1225
|
-
metadata: {}
|
|
1240
|
+
metadata: {},
|
|
1241
|
+
softExpiresAt: args.softExpiresAt ?? null
|
|
1226
1242
|
};
|
|
1227
1243
|
}
|
|
1228
1244
|
|
|
1229
1245
|
// src/core/engine.ts
|
|
1246
|
+
var SOFT_EXPIRY_GRACE_MS = 2e3;
|
|
1247
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1230
1248
|
function createMessagingEngine(options) {
|
|
1231
1249
|
const { repository, manager, identity } = options;
|
|
1232
|
-
const
|
|
1250
|
+
const browserNow = options.now ?? Date.now;
|
|
1251
|
+
let serverClockOffsetMs = 0;
|
|
1252
|
+
const now = () => browserNow() + serverClockOffsetMs;
|
|
1253
|
+
function learnServerClock(serverNow) {
|
|
1254
|
+
if (serverNow === null) return;
|
|
1255
|
+
const server = Date.parse(serverNow);
|
|
1256
|
+
if (Number.isFinite(server)) serverClockOffsetMs = server - browserNow();
|
|
1257
|
+
}
|
|
1258
|
+
const store = createMessagingStore({ now });
|
|
1233
1259
|
const conversationPageSize = options.conversationPageSize ?? 30;
|
|
1234
1260
|
const messagePageSize = options.messagePageSize ?? 50;
|
|
1235
1261
|
const openChannels = /* @__PURE__ */ new Map();
|
|
@@ -1243,6 +1269,8 @@ function createMessagingEngine(options) {
|
|
|
1243
1269
|
let inboxChannel = null;
|
|
1244
1270
|
let conversationCursor = null;
|
|
1245
1271
|
let disposed = false;
|
|
1272
|
+
const pendingExpiry = /* @__PURE__ */ new Map();
|
|
1273
|
+
let expiryTimer = null;
|
|
1246
1274
|
function report(event) {
|
|
1247
1275
|
options.onDiagnostic?.(event);
|
|
1248
1276
|
}
|
|
@@ -1285,6 +1313,72 @@ function createMessagingEngine(options) {
|
|
|
1285
1313
|
}).catch(() => void 0);
|
|
1286
1314
|
return current;
|
|
1287
1315
|
}
|
|
1316
|
+
function scheduleExpiryTimer() {
|
|
1317
|
+
if (expiryTimer !== null) {
|
|
1318
|
+
clearTimeout(expiryTimer);
|
|
1319
|
+
expiryTimer = null;
|
|
1320
|
+
}
|
|
1321
|
+
if (disposed || pendingExpiry.size === 0) return;
|
|
1322
|
+
const soonest = Math.min(...pendingExpiry.values());
|
|
1323
|
+
const delay = Math.min(
|
|
1324
|
+
MAX_TIMER_DELAY_MS,
|
|
1325
|
+
Math.max(0, soonest + SOFT_EXPIRY_GRACE_MS - now())
|
|
1326
|
+
);
|
|
1327
|
+
expiryTimer = setTimeout(() => {
|
|
1328
|
+
expiryTimer = null;
|
|
1329
|
+
void fireSoftExpiry();
|
|
1330
|
+
}, delay);
|
|
1331
|
+
}
|
|
1332
|
+
function noteSoftExpiry(id, at) {
|
|
1333
|
+
if (at === null) return;
|
|
1334
|
+
if (at <= now() && (store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const held = pendingExpiry.get(id);
|
|
1338
|
+
if (held !== void 0 && held <= at) return;
|
|
1339
|
+
pendingExpiry.set(id, at);
|
|
1340
|
+
scheduleExpiryTimer();
|
|
1341
|
+
}
|
|
1342
|
+
async function refreshPendingExpiries(ids) {
|
|
1343
|
+
const summaries = store.snapshot().conversationSummaries;
|
|
1344
|
+
const counted = ids.filter((id) => (summaries.get(id)?.unreadCount ?? 0) > 0);
|
|
1345
|
+
ids.forEach((id) => pendingExpiry.delete(id));
|
|
1346
|
+
if (counted.length > 0) {
|
|
1347
|
+
const pending = await repository.pendingSoftExpiries(counted);
|
|
1348
|
+
if (disposed) return;
|
|
1349
|
+
learnServerClock(pending.serverNow);
|
|
1350
|
+
pending.byConversation.forEach(
|
|
1351
|
+
(at, id) => noteSoftExpiry(id, softExpiryTime({ softExpiresAt: at }))
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
scheduleExpiryTimer();
|
|
1355
|
+
}
|
|
1356
|
+
async function fireSoftExpiry() {
|
|
1357
|
+
if (disposed) return;
|
|
1358
|
+
const at = now();
|
|
1359
|
+
const due = [...pendingExpiry.entries()].filter(([, expiry]) => expiry <= at).map(([id]) => id);
|
|
1360
|
+
due.forEach((id) => pendingExpiry.delete(id));
|
|
1361
|
+
await Promise.all(
|
|
1362
|
+
due.map(async (id) => {
|
|
1363
|
+
if ((store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) return;
|
|
1364
|
+
try {
|
|
1365
|
+
const count = await repository.unreadCount(id);
|
|
1366
|
+
if (!disposed) store.setUnreadCount(id, count);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
reportError(error, "softExpiryUnread", { conversationId: id });
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
);
|
|
1372
|
+
if (disposed) return;
|
|
1373
|
+
await refreshPendingExpiries(due).catch(
|
|
1374
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
function watchSoftExpiries(ids) {
|
|
1378
|
+
void refreshPendingExpiries(ids).catch(
|
|
1379
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1288
1382
|
const outbox = createOutbox({
|
|
1289
1383
|
...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
|
|
1290
1384
|
send: (draft, clientMessageId) => repository.insertMessage(draft, clientMessageId),
|
|
@@ -1311,7 +1405,8 @@ function createMessagingEngine(options) {
|
|
|
1311
1405
|
...entry.draft.replyToId !== void 0 ? { replyToId: entry.draft.replyToId } : {},
|
|
1312
1406
|
...entry.draft.action !== void 0 ? { action: entry.draft.action } : {},
|
|
1313
1407
|
...entry.draft.attachments !== void 0 ? { attachments: [...entry.draft.attachments] } : {},
|
|
1314
|
-
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {}
|
|
1408
|
+
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {},
|
|
1409
|
+
...entry.draft.softExpiresAt !== void 0 ? { softExpiresAt: entry.draft.softExpiresAt } : {}
|
|
1315
1410
|
})
|
|
1316
1411
|
);
|
|
1317
1412
|
});
|
|
@@ -1340,6 +1435,7 @@ function createMessagingEngine(options) {
|
|
|
1340
1435
|
if (disposed) return;
|
|
1341
1436
|
conversationCursor = page.nextCursor;
|
|
1342
1437
|
store.setConversations(page.items, page.hasMore);
|
|
1438
|
+
watchSoftExpiries([...store.snapshot().conversationSummaries.keys()]);
|
|
1343
1439
|
}
|
|
1344
1440
|
async function reloadArchivedCount() {
|
|
1345
1441
|
if (disposed) return;
|
|
@@ -1534,6 +1630,7 @@ function createMessagingEngine(options) {
|
|
|
1534
1630
|
store.ingest(message);
|
|
1535
1631
|
const isMine = message.senderId === identity.userId;
|
|
1536
1632
|
store.applyLastMessage(message, { incrementUnread: !isMine });
|
|
1633
|
+
if (!isMine) noteSoftExpiry(message.conversationId, softExpiryTime(message));
|
|
1537
1634
|
if (!isMine && store.snapshot().activeConversationId === message.conversationId) {
|
|
1538
1635
|
void persistRead(message.conversationId).catch(() => void 0);
|
|
1539
1636
|
}
|
|
@@ -1595,6 +1692,7 @@ function createMessagingEngine(options) {
|
|
|
1595
1692
|
});
|
|
1596
1693
|
conversationCursor = page.nextCursor;
|
|
1597
1694
|
store.appendConversations(page.items, page.hasMore);
|
|
1695
|
+
watchSoftExpiries(page.items.map((item) => item.conversation.id));
|
|
1598
1696
|
} catch (error) {
|
|
1599
1697
|
reportError(error, "loadMoreConversations");
|
|
1600
1698
|
}
|
|
@@ -1614,7 +1712,10 @@ function createMessagingEngine(options) {
|
|
|
1614
1712
|
const directSummary = missingSummary ? await repository.getConversationSummary(id) : null;
|
|
1615
1713
|
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1616
1714
|
store.setConversationReadFailure(id, null);
|
|
1617
|
-
if (directSummary !== null)
|
|
1715
|
+
if (directSummary !== null) {
|
|
1716
|
+
store.setConversationSummary(directSummary);
|
|
1717
|
+
watchSoftExpiries([id]);
|
|
1718
|
+
}
|
|
1618
1719
|
await persistRead(
|
|
1619
1720
|
id,
|
|
1620
1721
|
() => openIsLive(id, generation, ownedChannel)
|
|
@@ -1709,6 +1810,9 @@ function createMessagingEngine(options) {
|
|
|
1709
1810
|
},
|
|
1710
1811
|
dispose() {
|
|
1711
1812
|
disposed = true;
|
|
1813
|
+
if (expiryTimer !== null) clearTimeout(expiryTimer);
|
|
1814
|
+
expiryTimer = null;
|
|
1815
|
+
pendingExpiry.clear();
|
|
1712
1816
|
openChannels.forEach((_handle, id) => {
|
|
1713
1817
|
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1714
1818
|
});
|
|
@@ -1927,10 +2031,13 @@ var RPCS = {
|
|
|
1927
2031
|
/** Conversation list + participants + last message + unread, keyset paged. */
|
|
1928
2032
|
conversationsWithDetails: "get_dm_conversations_with_details",
|
|
1929
2033
|
unreadCount: "get_dm_unread_count",
|
|
2034
|
+
/** The soonest future soft expiry per conversation, plus the server's clock. */
|
|
2035
|
+
pendingSoftExpiries: "get_dm_pending_soft_expiries",
|
|
1930
2036
|
userInfo: "get_dm_user_info",
|
|
1931
2037
|
isParticipant: "is_dm_participant"
|
|
1932
2038
|
};
|
|
1933
|
-
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";
|
|
2039
|
+
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";
|
|
2040
|
+
var PENDING_SOFT_EXPIRY_CHUNK = 200;
|
|
1934
2041
|
var PARTICIPANT_COLUMNS = "conversation_id,user_id,role,joined_at,last_read_at,is_muted,is_archived";
|
|
1935
2042
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
1936
2043
|
function optionalOrg(organizationId) {
|
|
@@ -2351,6 +2458,9 @@ function createMessagingRepository(options) {
|
|
|
2351
2458
|
// confirmed row instead of showing the message twice.
|
|
2352
2459
|
client_message_id: clientMessageId,
|
|
2353
2460
|
action_data: draft.action ?? null,
|
|
2461
|
+
// SOFT EXPIRY: written only when the sender sets one. Lapsed is
|
|
2462
|
+
// derived from it live (`isLapsed`), never stamped.
|
|
2463
|
+
...draft.softExpiresAt !== void 0 ? { soft_expires_at: draft.softExpiresAt } : {},
|
|
2354
2464
|
metadata: {
|
|
2355
2465
|
...draft.metadata ?? {},
|
|
2356
2466
|
...draft.attachments !== void 0 && draft.attachments.length > 0 ? { attachments: draft.attachments } : {},
|
|
@@ -2403,6 +2513,56 @@ function createMessagingRepository(options) {
|
|
|
2403
2513
|
}
|
|
2404
2514
|
return Math.floor(unread);
|
|
2405
2515
|
},
|
|
2516
|
+
async unreadCount(conversationId) {
|
|
2517
|
+
const operation = "unreadCount";
|
|
2518
|
+
const me = await actingUser(operation);
|
|
2519
|
+
const unread = await withSessionRetry(
|
|
2520
|
+
operation,
|
|
2521
|
+
() => rpc(
|
|
2522
|
+
RPCS.unreadCount,
|
|
2523
|
+
{ p_conversation_id: conversationId, p_user_id: me },
|
|
2524
|
+
operation
|
|
2525
|
+
)
|
|
2526
|
+
);
|
|
2527
|
+
if (typeof unread !== "number" || !Number.isFinite(unread) || unread < 0) {
|
|
2528
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unread)}`);
|
|
2529
|
+
}
|
|
2530
|
+
return Math.floor(unread);
|
|
2531
|
+
},
|
|
2532
|
+
async pendingSoftExpiries(conversationIds) {
|
|
2533
|
+
const operation = "pendingSoftExpiries";
|
|
2534
|
+
const unique = [...new Set(conversationIds)];
|
|
2535
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
2536
|
+
let serverNow = null;
|
|
2537
|
+
for (let start = 0; start < unique.length; start += PENDING_SOFT_EXPIRY_CHUNK) {
|
|
2538
|
+
const chunk = unique.slice(start, start + PENDING_SOFT_EXPIRY_CHUNK);
|
|
2539
|
+
const answer = await withSessionRetry(
|
|
2540
|
+
operation,
|
|
2541
|
+
() => rpc(
|
|
2542
|
+
RPCS.pendingSoftExpiries,
|
|
2543
|
+
{ p_conversation_ids: chunk },
|
|
2544
|
+
operation
|
|
2545
|
+
)
|
|
2546
|
+
);
|
|
2547
|
+
const now = answer?.["server_now"];
|
|
2548
|
+
const pending = answer?.["pending"];
|
|
2549
|
+
if (typeof now !== "string" || !Array.isArray(pending)) {
|
|
2550
|
+
throw invalidResponse(
|
|
2551
|
+
operation,
|
|
2552
|
+
`${RPCS.pendingSoftExpiries} returned ${JSON.stringify(answer)}`
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
serverNow = now;
|
|
2556
|
+
for (const row of pending) {
|
|
2557
|
+
const id = row["conversation_id"];
|
|
2558
|
+
const at = row["soft_expires_at"];
|
|
2559
|
+
if (typeof id === "string" && typeof at === "string") {
|
|
2560
|
+
byConversation.set(id, at);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return { byConversation, serverNow };
|
|
2565
|
+
},
|
|
2406
2566
|
async setConversationFlags(conversationId, flags) {
|
|
2407
2567
|
const operation = "setConversationFlags";
|
|
2408
2568
|
const me = await actingUser(operation);
|
|
@@ -3814,6 +3974,23 @@ function ConversationRow(props) {
|
|
|
3814
3974
|
}
|
|
3815
3975
|
);
|
|
3816
3976
|
}
|
|
3977
|
+
function useLapseClock(messages) {
|
|
3978
|
+
const [now, setNow] = useState2(() => Date.now());
|
|
3979
|
+
let soonest = null;
|
|
3980
|
+
for (const message of messages) {
|
|
3981
|
+
const at = softExpiryTime(message);
|
|
3982
|
+
if (at !== null && at > now && (soonest === null || at < soonest)) soonest = at;
|
|
3983
|
+
}
|
|
3984
|
+
useEffect3(() => {
|
|
3985
|
+
if (soonest === null) return;
|
|
3986
|
+
const timer = setTimeout(
|
|
3987
|
+
() => setNow(Date.now()),
|
|
3988
|
+
Math.min(2147483647, Math.max(0, soonest - Date.now()))
|
|
3989
|
+
);
|
|
3990
|
+
return () => clearTimeout(timer);
|
|
3991
|
+
}, [soonest]);
|
|
3992
|
+
return now;
|
|
3993
|
+
}
|
|
3817
3994
|
function ConversationView(props) {
|
|
3818
3995
|
const { conversationId } = props;
|
|
3819
3996
|
const host = useMessagingHost();
|
|
@@ -3826,6 +4003,7 @@ function ConversationView(props) {
|
|
|
3826
4003
|
const selfId = host?.identity.userId ?? "";
|
|
3827
4004
|
const messages = conversation.messages;
|
|
3828
4005
|
const lastId = messages.at(-1)?.id ?? null;
|
|
4006
|
+
const lapseNow = useLapseClock(messages);
|
|
3829
4007
|
useEffect3(() => {
|
|
3830
4008
|
const node = scrollRef.current;
|
|
3831
4009
|
if (node === null) return;
|
|
@@ -3957,6 +4135,7 @@ function ConversationView(props) {
|
|
|
3957
4135
|
MessageGroupView,
|
|
3958
4136
|
{
|
|
3959
4137
|
allMessages: messages,
|
|
4138
|
+
now: lapseNow,
|
|
3960
4139
|
showAuthor: conversation.summary?.conversation.type !== "direct",
|
|
3961
4140
|
messages: group.messages,
|
|
3962
4141
|
actor,
|
|
@@ -4017,6 +4196,7 @@ function MessageGroupView(props) {
|
|
|
4017
4196
|
replyTo: props.allMessages.find((parent) => parent.id === message.replyToId),
|
|
4018
4197
|
isMine: props.isMine,
|
|
4019
4198
|
isLast: index === props.messages.length - 1,
|
|
4199
|
+
now: props.now,
|
|
4020
4200
|
onRetry: props.onRetry,
|
|
4021
4201
|
onReply: props.onReply,
|
|
4022
4202
|
failedEntryId: message.clientMessageId !== null ? props.failedEntryIdByClientId.get(message.clientMessageId) ?? null : null
|
|
@@ -4036,12 +4216,15 @@ function MessageBubble(props) {
|
|
|
4036
4216
|
if (message.deletedAt !== null) {
|
|
4037
4217
|
return /* @__PURE__ */ jsx4("div", { className: "mx-msg__bubble mx-msg__bubble--deleted", children: "Message deleted" });
|
|
4038
4218
|
}
|
|
4219
|
+
const lapsed = isLapsed(message, props.now ?? Date.now());
|
|
4039
4220
|
const classes = [
|
|
4040
4221
|
"mx-msg__bubble",
|
|
4041
4222
|
isMine ? "mx-msg__bubble--mine" : "",
|
|
4042
4223
|
props.isLast ? "mx-msg__bubble--tail" : "",
|
|
4043
4224
|
message.deliveryState === "sending" ? "mx-msg__bubble--pending" : "",
|
|
4044
|
-
message.deliveryState === "failed" ? "mx-msg__bubble--failed" : ""
|
|
4225
|
+
message.deliveryState === "failed" ? "mx-msg__bubble--failed" : "",
|
|
4226
|
+
// SOFT EXPIRY: past, still fully readable and actionable — muted, never hidden.
|
|
4227
|
+
lapsed ? "mx-msg__bubble--lapsed" : ""
|
|
4045
4228
|
].filter(Boolean).join(" ");
|
|
4046
4229
|
const bubble = /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
4047
4230
|
/* @__PURE__ */ jsxs2("div", { className: classes, title: formatMessageTime(message.createdAt), children: [
|
|
@@ -4091,6 +4274,17 @@ function MessageBubble(props) {
|
|
|
4091
4274
|
children: [
|
|
4092
4275
|
/* @__PURE__ */ jsx4("span", { className: "mx-msg__message-time", children: props.isLast ? formatMessageTime(message.createdAt) : null }),
|
|
4093
4276
|
message.editedAt !== null ? " \xB7 edited" : null,
|
|
4277
|
+
lapsed && message.softExpiresAt !== null ? /* @__PURE__ */ jsxs2(
|
|
4278
|
+
"span",
|
|
4279
|
+
{
|
|
4280
|
+
className: "mx-msg__lapsed",
|
|
4281
|
+
title: `Past ${formatMessageTime(message.softExpiresAt)}`,
|
|
4282
|
+
children: [
|
|
4283
|
+
/* @__PURE__ */ jsx4(ClockIcon, {}),
|
|
4284
|
+
"Past"
|
|
4285
|
+
]
|
|
4286
|
+
}
|
|
4287
|
+
) : null,
|
|
4094
4288
|
isMine ? /* @__PURE__ */ jsx4(DeliveryTick, { state: message.deliveryState }) : null,
|
|
4095
4289
|
message.deliveryState === "failed" && props.failedEntryId != null && props.onRetry !== void 0 ? /* @__PURE__ */ jsx4(
|
|
4096
4290
|
"button",
|
|
@@ -4367,6 +4561,7 @@ export {
|
|
|
4367
4561
|
groupMessages,
|
|
4368
4562
|
inboxTopic,
|
|
4369
4563
|
invalidResponse,
|
|
4564
|
+
isLapsed,
|
|
4370
4565
|
isSameDay,
|
|
4371
4566
|
messagingClientId,
|
|
4372
4567
|
normalizeMessagingError,
|
|
@@ -4378,6 +4573,7 @@ export {
|
|
|
4378
4573
|
projectParticipantRole,
|
|
4379
4574
|
projectUserSummary,
|
|
4380
4575
|
resolveActor,
|
|
4576
|
+
softExpiryTime,
|
|
4381
4577
|
splitText,
|
|
4382
4578
|
summarizeText,
|
|
4383
4579
|
toMessagingArchiveFilter,
|