@glyphteck/veyl 0.77.0 → 0.78.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/dist/index.js CHANGED
@@ -5855,7 +5855,7 @@ var init_canonical = __esm(() => {
5855
5855
  });
5856
5856
 
5857
5857
  // ../../core/config.js
5858
- var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, ACCOUNT_CONNECTION_SLOW_MS, ACCOUNT_CONNECTION_UNAVAILABLE_MS, ACCOUNT_CONNECTION_RETRY_MIN_MS, ACCOUNT_CONNECTION_RETRY_MAX_MS, TICKET_MESSAGE_LIMIT = 4000, TICKET_ATTACHMENT_LIMIT = 3, TICKET_ATTACHMENT_MAX_BYTES, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, BTC_PRICE_FALLBACK = 80000, CHAT_MESSAGE_EDIT_WINDOW_MS, CHAT_SEND_CONNECTION_WAIT_MS, CALL_SPEAKING_THRESHOLD = 0.02, CALL_SPEAKING_HOLD_MS = 320, LOCAL_CACHE_WRITE_DELAY_MS = 350, WEB_LAYOUT_SAVE_DELAY_MS, WEB_CHAT_LAYOUT_LIMIT = 32, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, AVATAR_IMAGE_QUALITY_ATTEMPTS, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS = 256, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_ADMISSION_CACHE_PEERS = 1024, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_DISCOVERY_PARALLEL_CHATS, CHAT_INBOX_SETTLEMENT_PARALLEL_CHATS = 4, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_COMPLETE_SYNC_DELAY_MS = 250, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, LIVE_RECONNECT_MIN_MS = 500, LIVE_RECONNECT_MAX_MS, LIVE_RECONNECT_STABLE_MS, LIVE_READY_TIMEOUT_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_TITLE_STATE_MAX_CHARS = 128, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_LARGE_GROUP_MENTION_MAX_TARGETS = 16, CHAT_MAX_TEXT_CHARS = 2048, CHAT_TITLE_INPUT_MAX_CHARS = 16, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_TRANSFER_REPAIR_MS, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS = 100, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TOKEN_HISTORY_PAGE_SIZE = 50, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS, BITCOIN_POLL_MS = 60000, BITCOIN_FETCH_TIMEOUT_MS = 8000, BITCOIN_FEES_MAX_AGE_MS;
5858
+ var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, ACCOUNT_CONNECTION_SLOW_MS, ACCOUNT_CONNECTION_UNAVAILABLE_MS, ACCOUNT_CONNECTION_RETRY_MIN_MS, ACCOUNT_CONNECTION_RETRY_MAX_MS, TICKET_MESSAGE_LIMIT = 4000, TICKET_ATTACHMENT_LIMIT = 3, TICKET_ATTACHMENT_MAX_BYTES, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, BTC_PRICE_FALLBACK = 80000, CHAT_MESSAGE_EDIT_WINDOW_MS, CHAT_SEND_CONNECTION_WAIT_MS, CALL_SPEAKING_THRESHOLD = 0.02, CALL_SPEAKING_HOLD_MS = 320, LOCAL_CACHE_WRITE_DELAY_MS = 350, WEB_LAYOUT_SAVE_DELAY_MS, WEB_CHAT_LAYOUT_LIMIT = 32, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, AVATAR_IMAGE_QUALITY_ATTEMPTS, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS = 256, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_ADMISSION_CACHE_PEERS = 1024, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_DISCOVERY_PARALLEL_CHATS, CHAT_INBOX_SETTLEMENT_PARALLEL_CHATS = 4, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_COMPLETE_SYNC_DELAY_MS = 250, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, LIVE_RECONNECT_MIN_MS = 500, LIVE_RECONNECT_MAX_MS, LIVE_RECONNECT_STABLE_MS, LIVE_READY_TIMEOUT_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_TITLE_STATE_MAX_CHARS = 128, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_LARGE_GROUP_MENTION_MAX_TARGETS = 16, CHAT_MAX_TEXT_CHARS = 2048, CHAT_TITLE_INPUT_MAX_CHARS = 16, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_TRANSFER_REPAIR_MS, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS = 100, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TOKEN_HISTORY_PAGE_SIZE = 50, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS, BITCOIN_POLL_MS = 60000, BITCOIN_FETCH_TIMEOUT_MS = 8000, BITCOIN_FEES_MAX_AGE_MS, CALL_MAX_VIDEO_SOURCES = 4, CALL_MAX_VIDEO_SOURCES_PER_PARTICIPANT = 1;
5859
5859
  var init_config = __esm(() => {
5860
5860
  MINUTE_MS = 60 * MS_PER_SECOND;
5861
5861
  HOUR_MS = 60 * MINUTE_MS;
@@ -6126,150 +6126,6 @@ function sameText(left, right) {
6126
6126
  return lowerText(left) === lowerText(right);
6127
6127
  }
6128
6128
 
6129
- // ../../core/utils/time.js
6130
- function timestampMs(value, fallback = null, options = {}) {
6131
- let ms = null;
6132
- if (typeof value?.toMillis === "function") {
6133
- ms = value.toMillis();
6134
- } else if (value instanceof Date) {
6135
- ms = value.getTime();
6136
- } else if (typeof value?.seconds === "number") {
6137
- ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
6138
- } else if (typeof value?._seconds === "number") {
6139
- ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
6140
- } else if (Number.isFinite(value)) {
6141
- ms = value;
6142
- } else if (options.parseString && typeof value === "string") {
6143
- const numberMs = Number(value);
6144
- ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
6145
- }
6146
- if (!Number.isFinite(ms) || options.positive && ms <= 0) {
6147
- return fallback;
6148
- }
6149
- return ms;
6150
- }
6151
- function timestampKey(value) {
6152
- if (value == null) {
6153
- return null;
6154
- }
6155
- return timestampMs(value, null) ?? String(value);
6156
- }
6157
- function makeTimestamp(ms) {
6158
- return {
6159
- toMillis() {
6160
- return ms;
6161
- },
6162
- toDate() {
6163
- return new Date(ms);
6164
- }
6165
- };
6166
- }
6167
- function twoDigits(value) {
6168
- return String(value).padStart(2, "0");
6169
- }
6170
- function dayKey(date) {
6171
- return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
6172
- }
6173
- function localDayKey(value) {
6174
- const ms = timestampMs(value, null, { parseString: true });
6175
- if (!Number.isFinite(ms))
6176
- return "";
6177
- return dayKey(new Date(ms));
6178
- }
6179
- function hourKey(dateOrHour) {
6180
- const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
6181
- return twoDigits(hour);
6182
- }
6183
- function dayHourKey(date) {
6184
- return `${dayKey(date)}-${hourKey(date)}`;
6185
- }
6186
- function nextLocalDayStartMs(ms) {
6187
- const date = new Date(ms);
6188
- date.setDate(date.getDate() + 1);
6189
- date.setHours(0, 0, 0, 0);
6190
- return date.getTime();
6191
- }
6192
- function nextRowDateTimeRefreshMs(value, now = Date.now()) {
6193
- const ms = timestampMs(value, null, { parseString: true });
6194
- const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
6195
- if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
6196
- return null;
6197
- const age = nowMs2 - ms;
6198
- if (age < -MINUTE_MS2)
6199
- return ms - MINUTE_MS2;
6200
- if (age < MINUTE_MS2)
6201
- return ms + MINUTE_MS2;
6202
- if (age < HOUR_MS2)
6203
- return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
6204
- if (localDayKey(ms) === localDayKey(nowMs2))
6205
- return nextLocalDayStartMs(nowMs2);
6206
- return null;
6207
- }
6208
- function nextRelativeTimeRefreshMs(value, now = Date.now()) {
6209
- const ms = timestampMs(value, null, { parseString: true });
6210
- const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
6211
- if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
6212
- return null;
6213
- const age = Math.max(0, nowMs2 - ms);
6214
- if (age < 30000)
6215
- return ms + 30000;
6216
- if (age < 60 * 60000)
6217
- return ms + (Math.floor(age / 60000) + 1) * 60000;
6218
- if (age < 24 * 60 * 60000)
6219
- return ms + (Math.floor(age / (60 * 60000)) + 1) * 60 * 60000;
6220
- return ms + (Math.floor(age / (24 * 60 * 60000)) + 1) * 24 * 60 * 60000;
6221
- }
6222
- var MINUTE_MS2 = 60000, HOUR_MS2;
6223
- var init_time = __esm(() => {
6224
- HOUR_MS2 = 60 * MINUTE_MS2;
6225
- });
6226
-
6227
- // ../../core/chat/state.js
6228
- function makeCid() {
6229
- return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
6230
- }
6231
- function getMessageKey(message) {
6232
- return message?.cid || message?.id || null;
6233
- }
6234
- function getCidMs(cid) {
6235
- if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
6236
- return null;
6237
- }
6238
- const base = cid.slice(0, -6);
6239
- const ms = Number.parseInt(base, 36);
6240
- return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
6241
- }
6242
- function getMessageOrderMs(message) {
6243
- return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
6244
- }
6245
- function sortMessages(messages) {
6246
- return [...messages].sort((a, b) => {
6247
- const aMs = getMessageOrderMs(a);
6248
- const bMs = getMessageOrderMs(b);
6249
- if (aMs !== bMs) {
6250
- return aMs - bMs;
6251
- }
6252
- return String(a?.id || "").localeCompare(String(b?.id || ""));
6253
- });
6254
- }
6255
- function mergeMessages(...groups) {
6256
- const merged = new Map;
6257
- for (const group of groups) {
6258
- for (const message of group || []) {
6259
- const key = getMessageKey(message);
6260
- if (!key) {
6261
- continue;
6262
- }
6263
- merged.set(key, message);
6264
- }
6265
- }
6266
- return sortMessages([...merged.values()]);
6267
- }
6268
- var init_state = __esm(() => {
6269
- init_core();
6270
- init_time();
6271
- });
6272
-
6273
6129
  // ../../core/chat/messages/call.js
6274
6130
  function readCallMessage(message) {
6275
6131
  if (message?.t !== CALL_MSG_TYPE || typeof message.callId !== "string" || !/^[0-9a-f]{64}$/u.test(message.callId) || !["started", "ended"].includes(message.event))
@@ -6286,17 +6142,7 @@ function callMessageText(message) {
6286
6142
  const call = readCallMessage(message);
6287
6143
  return call ? `call ${call.event}` : "";
6288
6144
  }
6289
- function latestCallStartKey(messages) {
6290
- for (let index = messages.length - 1;index >= 0; index -= 1) {
6291
- if (readCallMessage(messages[index])?.event === "started")
6292
- return getMessageKey(messages[index]);
6293
- }
6294
- return "";
6295
- }
6296
6145
  var CALL_MSG_TYPE = "call";
6297
- var init_call = __esm(() => {
6298
- init_state();
6299
- });
6300
6146
 
6301
6147
  // ../../core/chat/messages/types.js
6302
6148
  var ATTACHMENT_MSG_TYPES, MAX_TXT_CHARS, REACTION_MSG_TYPE = "rxn", DELETE_MSG_TYPE = "del", SYSTEM_MSG_TYPE = "sys", EPOCH_TRANSITION_MSG_TYPE = "epoch", EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal", SYSTEM_SETTINGS_KIND = "settings", DEFAULT_REACTION_EMOJI = "❤️", MAX_REACTIONS, HOLD_VISIBLE_KEY = "__holdVisible", SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
@@ -6547,7 +6393,6 @@ var init_notifications = __esm(() => {
6547
6393
  init_seed();
6548
6394
  init_sign();
6549
6395
  init_config();
6550
- init_call();
6551
6396
  init_types();
6552
6397
  "use client";
6553
6398
  CHAT_NOTIFICATION_MODES = Object.freeze({
@@ -6857,6 +6702,7 @@ var init_origins = __esm(() => {
6857
6702
  root: ROOT_DOMAIN,
6858
6703
  rootDev: `dev.${ROOT_DOMAIN}`,
6859
6704
  veyl: `veyl.${ROOT_DOMAIN}`,
6705
+ veylOpen: `open.veyl.${ROOT_DOMAIN}`,
6860
6706
  veylDev: `dev.veyl.${ROOT_DOMAIN}`,
6861
6707
  live: `live.veyl.${ROOT_DOMAIN}`,
6862
6708
  liveDev: `live.dev.veyl.${ROOT_DOMAIN}`,
@@ -6868,6 +6714,7 @@ var init_origins = __esm(() => {
6868
6714
  root: `https://${domains.root}`,
6869
6715
  rootDev: `https://${domains.rootDev}`,
6870
6716
  veyl: `https://${domains.veyl}`,
6717
+ veylOpen: `https://${domains.veylOpen}`,
6871
6718
  veylDev: `https://${domains.veylDev}`,
6872
6719
  veylDevWeb: getVeylDevWebOrigin(VEYL_DEV_WEB_PORT_MIN)
6873
6720
  });
@@ -6882,7 +6729,8 @@ var init_origins = __esm(() => {
6882
6729
  ]);
6883
6730
  appLinkDomains = Object.freeze([
6884
6731
  domains.veyl,
6885
- domains.veylDev
6732
+ domains.veylDev,
6733
+ domains.veylOpen
6886
6734
  ]);
6887
6735
  });
6888
6736
 
@@ -7011,9 +6859,6 @@ function normalizeSettings(settings, base = defaultSettings) {
7011
6859
  if (!MONEY_FORMATS.includes(next.moneyFormat)) {
7012
6860
  throw new SettingsValidationError("money-format", "bad moneyFormat");
7013
6861
  }
7014
- if (!WEB_LAYOUTS.includes(next.webLayout)) {
7015
- throw new SettingsValidationError("web-layout", "bad webLayout");
7016
- }
7017
6862
  if (typeof next.glass !== "boolean") {
7018
6863
  throw new SettingsValidationError("glass", "glass must be boolean");
7019
6864
  }
@@ -7035,7 +6880,7 @@ function normalizeSettings(settings, base = defaultSettings) {
7035
6880
  if (next.faceID !== null && typeof next.faceID !== "boolean") {
7036
6881
  throw new SettingsValidationError("face-id", "faceID must be boolean or null");
7037
6882
  }
7038
- return next;
6883
+ return Object.fromEntries(Object.keys(defaultSettings).map((key) => [key, next[key]]));
7039
6884
  }
7040
6885
  function normalizeCallAudio(patch, base = defaultSettings.callAudio) {
7041
6886
  if (patch !== undefined && (!patch || typeof patch !== "object" || Array.isArray(patch)))
@@ -7063,12 +6908,11 @@ function normalizePeerAudio(patch, base = {}) {
7063
6908
  }
7064
6909
  return Object.fromEntries(entries);
7065
6910
  }
7066
- var SEND_ON_SCAN_ENABLED = false, WEB_LAYOUTS, WEB_SIDEBAR_WIDTH, DEFAULT_WEB_CHAT_LAYOUT, WALLET_NETWORKS, SettingsValidationError, defaultSettings;
6911
+ var SEND_ON_SCAN_ENABLED = false, WEB_SIDEBAR_WIDTH, DEFAULT_WEB_CHAT_LAYOUT, WALLET_NETWORKS, SettingsValidationError, defaultSettings;
7067
6912
  var init_settings = __esm(() => {
7068
6913
  init_config();
7069
6914
  init_money();
7070
6915
  init_network();
7071
- WEB_LAYOUTS = ["floating", "sidebar"];
7072
6916
  WEB_SIDEBAR_WIDTH = { default: 320, min: 64, max: 480 };
7073
6917
  DEFAULT_WEB_CHAT_LAYOUT = Object.freeze({ open: null, width: null });
7074
6918
  WALLET_NETWORKS = new Set([MAINNET_NETWORK, REGTEST_NETWORK]);
@@ -7080,7 +6924,6 @@ var init_settings = __esm(() => {
7080
6924
  };
7081
6925
  defaultSettings = {
7082
6926
  glass: true,
7083
- webLayout: "floating",
7084
6927
  sidebarWidth: WEB_SIDEBAR_WIDTH.default,
7085
6928
  walletChartHeight: null,
7086
6929
  webChatLayouts: [],
@@ -8495,15 +8338,13 @@ async function bootWallet(walletMnemonic, user, {
8495
8338
  decodeSparkAddress,
8496
8339
  network
8497
8340
  });
8498
- const initializeWallet = WalletClass.getOrCreateWallet || WalletClass.initialize;
8499
8341
  try {
8500
8342
  let wallet;
8501
8343
  const sparkStartedAt = Date.now();
8502
8344
  markDiag(diag, "vault.bootWallet.spark.start", {});
8503
- ({ wallet } = await initializeWallet.call(WalletClass, {
8345
+ ({ wallet } = await WalletClass.initialize({
8504
8346
  mnemonicOrSeed: walletMnemonic,
8505
8347
  accountNumber,
8506
- forceReinit: true,
8507
8348
  options: {
8508
8349
  log: false,
8509
8350
  network,
@@ -8928,7 +8769,14 @@ async function openVaultAccountSession(vault, password, options = {}) {
8928
8769
  return Promise.reject(new Error("account session closed"));
8929
8770
  if (connecting)
8930
8771
  return connecting;
8772
+ let releaseBoot;
8931
8773
  connecting = (async () => {
8774
+ const walletStartedAt = Date.now();
8775
+ mark(diag, "vault.unlock.wallet.start", { source });
8776
+ const boot = startWalletBoot(bootWallet2, mnemonicFromWalletEntropy(sessionWalletEntropy), currentUser, { network, diag });
8777
+ walletBoot = boot;
8778
+ releaseBoot = session.own(boot);
8779
+ boot.result.catch(() => {});
8932
8780
  const authStartedAt = Date.now();
8933
8781
  mark(diag, "vault.unlock.session.start", { source });
8934
8782
  await cloud.auth.user.getIdToken(true);
@@ -8962,11 +8810,6 @@ async function openVaultAccountSession(vault, password, options = {}) {
8962
8810
  requireCurrent(isCurrent);
8963
8811
  chatIdentitiesRegistered = true;
8964
8812
  }
8965
- const walletStartedAt = Date.now();
8966
- mark(diag, "vault.unlock.wallet.start", { source });
8967
- const boot = startWalletBoot(bootWallet2, mnemonicFromWalletEntropy(sessionWalletEntropy), currentUser, { network, diag });
8968
- walletBoot = boot;
8969
- const releaseBoot = session.own(boot);
8970
8813
  const walletReady = boot.result.then(async (walletIdentity) => {
8971
8814
  if (session.closed)
8972
8815
  throw new Error("account session closed");
@@ -8998,6 +8841,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
8998
8841
  walletReady.catch(() => {});
8999
8842
  return { walletReady };
9000
8843
  })().catch((error) => {
8844
+ releaseBoot?.();
9001
8845
  connecting = null;
9002
8846
  throw error;
9003
8847
  });
@@ -9061,6 +8905,104 @@ var init_agreement = __esm(() => {
9061
8905
  TERMS_VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}(?:\.\d+)?$/u;
9062
8906
  });
9063
8907
 
8908
+ // ../../core/utils/time.js
8909
+ function timestampMs(value, fallback = null, options = {}) {
8910
+ let ms = null;
8911
+ if (typeof value?.toMillis === "function") {
8912
+ ms = value.toMillis();
8913
+ } else if (value instanceof Date) {
8914
+ ms = value.getTime();
8915
+ } else if (typeof value?.seconds === "number") {
8916
+ ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
8917
+ } else if (typeof value?._seconds === "number") {
8918
+ ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
8919
+ } else if (Number.isFinite(value)) {
8920
+ ms = value;
8921
+ } else if (options.parseString && typeof value === "string") {
8922
+ const numberMs = Number(value);
8923
+ ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
8924
+ }
8925
+ if (!Number.isFinite(ms) || options.positive && ms <= 0) {
8926
+ return fallback;
8927
+ }
8928
+ return ms;
8929
+ }
8930
+ function timestampKey(value) {
8931
+ if (value == null) {
8932
+ return null;
8933
+ }
8934
+ return timestampMs(value, null) ?? String(value);
8935
+ }
8936
+ function makeTimestamp(ms) {
8937
+ return {
8938
+ toMillis() {
8939
+ return ms;
8940
+ },
8941
+ toDate() {
8942
+ return new Date(ms);
8943
+ }
8944
+ };
8945
+ }
8946
+ function twoDigits(value) {
8947
+ return String(value).padStart(2, "0");
8948
+ }
8949
+ function dayKey(date) {
8950
+ return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
8951
+ }
8952
+ function localDayKey(value) {
8953
+ const ms = timestampMs(value, null, { parseString: true });
8954
+ if (!Number.isFinite(ms))
8955
+ return "";
8956
+ return dayKey(new Date(ms));
8957
+ }
8958
+ function hourKey(dateOrHour) {
8959
+ const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
8960
+ return twoDigits(hour);
8961
+ }
8962
+ function dayHourKey(date) {
8963
+ return `${dayKey(date)}-${hourKey(date)}`;
8964
+ }
8965
+ function nextLocalDayStartMs(ms) {
8966
+ const date = new Date(ms);
8967
+ date.setDate(date.getDate() + 1);
8968
+ date.setHours(0, 0, 0, 0);
8969
+ return date.getTime();
8970
+ }
8971
+ function nextRowDateTimeRefreshMs(value, now = Date.now()) {
8972
+ const ms = timestampMs(value, null, { parseString: true });
8973
+ const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
8974
+ if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
8975
+ return null;
8976
+ const age = nowMs2 - ms;
8977
+ if (age < -MINUTE_MS2)
8978
+ return ms - MINUTE_MS2;
8979
+ if (age < MINUTE_MS2)
8980
+ return ms + MINUTE_MS2;
8981
+ if (age < HOUR_MS2)
8982
+ return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
8983
+ if (localDayKey(ms) === localDayKey(nowMs2))
8984
+ return nextLocalDayStartMs(nowMs2);
8985
+ return null;
8986
+ }
8987
+ function nextRelativeTimeRefreshMs(value, now = Date.now()) {
8988
+ const ms = timestampMs(value, null, { parseString: true });
8989
+ const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
8990
+ if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
8991
+ return null;
8992
+ const age = Math.max(0, nowMs2 - ms);
8993
+ if (age < 30000)
8994
+ return ms + 30000;
8995
+ if (age < 60 * 60000)
8996
+ return ms + (Math.floor(age / 60000) + 1) * 60000;
8997
+ if (age < 24 * 60 * 60000)
8998
+ return ms + (Math.floor(age / (60 * 60000)) + 1) * 60 * 60000;
8999
+ return ms + (Math.floor(age / (24 * 60 * 60000)) + 1) * 24 * 60 * 60000;
9000
+ }
9001
+ var MINUTE_MS2 = 60000, HOUR_MS2;
9002
+ var init_time = __esm(() => {
9003
+ HOUR_MS2 = 60 * MINUTE_MS2;
9004
+ });
9005
+
9064
9006
  // ../../core/moderation.js
9065
9007
  function banUntilMs(ban) {
9066
9008
  if (!ban || typeof ban !== "object" || Array.isArray(ban) || ban.until == null) {
@@ -9597,6 +9539,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
9597
9539
  let avatarPreview;
9598
9540
  let blockedSource = state.blocked;
9599
9541
  let blockedSet = new Set(blockedSource);
9542
+ const blockedChanges = new Map;
9600
9543
  let banTimer = null;
9601
9544
  let unsubscribeAuth = NOOP;
9602
9545
  let userWatches = [];
@@ -9610,11 +9553,27 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
9610
9553
  function makeSnapshot() {
9611
9554
  const bans = banState(state.banned);
9612
9555
  const userBan = bans.full || bans.chat;
9556
+ const visibleBlocked = new Set(state.blocked);
9557
+ const blockedPending = [];
9558
+ for (const [uid, change] of blockedChanges) {
9559
+ if (change.blocked)
9560
+ visibleBlocked.add(uid);
9561
+ else
9562
+ visibleBlocked.delete(uid);
9563
+ if (!change.written)
9564
+ blockedPending.push(uid);
9565
+ }
9566
+ if (visibleBlocked.size !== blockedSet.size || [...visibleBlocked].some((uid) => !blockedSet.has(uid))) {
9567
+ blockedSet = visibleBlocked;
9568
+ }
9569
+ const blocked = snapshot?.blocked && snapshot.blocked.length === blockedSet.size && snapshot.blocked.every((uid) => blockedSet.has(uid)) ? snapshot.blocked : [...blockedSet];
9613
9570
  return {
9614
9571
  ...state,
9615
9572
  avatar: avatarPreview === undefined ? state.avatar : avatarPreview,
9616
9573
  network: activeNetwork,
9617
9574
  blockedSet,
9575
+ blocked,
9576
+ blockedPending: snapshot?.blockedPending && snapshot.blockedPending.length === blockedPending.length && snapshot.blockedPending.every((uid, index) => uid === blockedPending[index]) ? snapshot.blockedPending : blockedPending,
9618
9577
  chatBanned: bans.chatBanned,
9619
9578
  avatarBanned: bans.avatarBanned,
9620
9579
  chatBanUntil: userBan?.until ?? null,
@@ -9898,6 +9857,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
9898
9857
  const authStartedAt = Date.now();
9899
9858
  const session = authSession + 1;
9900
9859
  authSession = session;
9860
+ blockedChanges.clear();
9901
9861
  closeUserWatches();
9902
9862
  markDiag(diag, "user.auth.state", { signedIn: !!authUser });
9903
9863
  if (!authUser) {
@@ -9997,6 +9957,20 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
9997
9957
  setState((user) => ({ ...user, banned: null }));
9998
9958
  })));
9999
9959
  userWatches.push(cloud.user.blocked.watch(authUser.uid, current((blocked, info = {}) => {
9960
+ if (info.pending || info.fromCache && state.blockedReady)
9961
+ return;
9962
+ const confirmed = new Set(blocked);
9963
+ for (const [uid, change] of blockedChanges) {
9964
+ if (!change.written)
9965
+ continue;
9966
+ if (!info.fromCache && confirmed.has(uid) === change.blocked)
9967
+ blockedChanges.delete(uid);
9968
+ else if (state.blocked.includes(uid))
9969
+ confirmed.add(uid);
9970
+ else
9971
+ confirmed.delete(uid);
9972
+ }
9973
+ blocked = [...confirmed];
10000
9974
  setState((user) => {
10001
9975
  const blockedReady = user.blockedReady || !info.fromCache;
10002
9976
  if (user.blockedReady === blockedReady && user.blocked.length === blocked.length && user.blocked.every((id, index) => id === blocked[index])) {
@@ -10072,6 +10046,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10072
10046
  if (!started)
10073
10047
  return;
10074
10048
  authSession += 1;
10049
+ blockedChanges.clear();
10075
10050
  closeUserWatches();
10076
10051
  unsubscribeAuth();
10077
10052
  setState((user) => ({ ...user, authError: null, authSessionError: null, profileError: null, settingsError: null }));
@@ -10093,6 +10068,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10093
10068
  agreementStored = null;
10094
10069
  agreementOverride = null;
10095
10070
  agreementAcceptance = null;
10071
+ blockedChanges.clear();
10096
10072
  userUid = null;
10097
10073
  avatarFetch = { uid: null, key: null, promise: null };
10098
10074
  avatarPreview = undefined;
@@ -10101,25 +10077,63 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10101
10077
  blockedSet = new Set(blockedSource);
10102
10078
  snapshot = makeSnapshot();
10103
10079
  }
10104
- function blockPeer(peer) {
10080
+ function changePeerBlock(peer, blocked, { beforeCommit } = {}) {
10105
10081
  const nextPeerUid = peerUid(peer);
10106
10082
  const uid = cloud.auth.user?.uid;
10083
+ const session = authSession;
10107
10084
  if (!uid)
10108
10085
  throw new Error("auth");
10109
10086
  if (!nextPeerUid)
10110
10087
  throw new Error("peer uid required");
10111
- if (nextPeerUid === uid)
10088
+ if (blocked && nextPeerUid === uid)
10112
10089
  return;
10113
- return cloud.user.blocked.add(uid, nextPeerUid);
10090
+ const prior = blockedChanges.get(nextPeerUid);
10091
+ if (prior && prior.blocked === blocked)
10092
+ return prior.promise;
10093
+ const current = () => isCurrentUser(uid, session);
10094
+ const assertCurrent = () => {
10095
+ if (!current())
10096
+ throw new Error("account changed during block update");
10097
+ };
10098
+ const change = { blocked, written: false, promise: null };
10099
+ change.promise = Promise.resolve().then(async () => {
10100
+ try {
10101
+ await prior?.promise.catch(NOOP);
10102
+ assertCurrent();
10103
+ await beforeCommit?.();
10104
+ assertCurrent();
10105
+ const result = await cloud.user.blocked[blocked ? "add" : "remove"](uid, nextPeerUid);
10106
+ assertCurrent();
10107
+ change.written = true;
10108
+ if (blockedChanges.get(nextPeerUid) === change && state.blocked.includes(nextPeerUid) === blocked) {
10109
+ blockedChanges.delete(nextPeerUid);
10110
+ }
10111
+ const confirmed = new Set(state.blocked);
10112
+ if (blocked)
10113
+ confirmed.add(nextPeerUid);
10114
+ else
10115
+ confirmed.delete(nextPeerUid);
10116
+ setState((user) => ({ ...user, blocked: [...confirmed] }));
10117
+ return result;
10118
+ } catch (error) {
10119
+ if (current() && blockedChanges.get(nextPeerUid) === change) {
10120
+ blockedChanges.delete(nextPeerUid);
10121
+ if (prior?.written)
10122
+ blockedChanges.set(nextPeerUid, prior);
10123
+ setState(state, true);
10124
+ }
10125
+ throw error;
10126
+ }
10127
+ });
10128
+ blockedChanges.set(nextPeerUid, change);
10129
+ setState(state, true);
10130
+ return change.promise;
10114
10131
  }
10115
- function unblockPeer(peer) {
10116
- const nextPeerUid = peerUid(peer);
10117
- const uid = cloud.auth.user?.uid;
10118
- if (!uid)
10119
- throw new Error("auth");
10120
- if (!nextPeerUid)
10121
- throw new Error("peer uid required");
10122
- return cloud.user.blocked.remove(uid, nextPeerUid);
10132
+ function blockPeer(peer, options) {
10133
+ return changePeerBlock(peer, true, options);
10134
+ }
10135
+ function unblockPeer(peer, options) {
10136
+ return changePeerBlock(peer, false, options);
10123
10137
  }
10124
10138
  async function acceptAgreement(requestedAgreement = CURRENT_AGREEMENT) {
10125
10139
  const uid = cloud.auth.user?.uid;
@@ -10333,6 +10347,13 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10333
10347
  syncAvatar();
10334
10348
  return true;
10335
10349
  }
10350
+ function restoreBlocked({ uid, blocked }) {
10351
+ if (!started || !uid || uid !== state.uid || cloud.auth.user?.uid !== uid)
10352
+ return;
10353
+ if (state.blockedReady || !Array.isArray(blocked))
10354
+ return;
10355
+ setState((user) => ({ ...user, blocked: [...blocked], blockedReady: true }));
10356
+ }
10336
10357
  snapshot = makeSnapshot();
10337
10358
  return {
10338
10359
  getSnapshot() {
@@ -10358,6 +10379,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10358
10379
  previewAvatar,
10359
10380
  confirmAvatar,
10360
10381
  restoreBootstrap,
10382
+ restoreBlocked,
10361
10383
  retry
10362
10384
  };
10363
10385
  }
@@ -22167,6 +22189,52 @@ var init_bootstrap = __esm(() => {
22167
22189
  PROFILE_FIELDS = ["username", "identities", "walletPK", "chatPK", "chatSigningPK", "notificationPK", "chatAdmission", "accountType", "hasAvatarEntry", "avatarVersion"];
22168
22190
  });
22169
22191
 
22192
+ // ../../core/chat/state.js
22193
+ function makeCid() {
22194
+ return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
22195
+ }
22196
+ function getMessageKey(message) {
22197
+ return message?.cid || message?.id || null;
22198
+ }
22199
+ function getCidMs(cid) {
22200
+ if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
22201
+ return null;
22202
+ }
22203
+ const base = cid.slice(0, -6);
22204
+ const ms = Number.parseInt(base, 36);
22205
+ return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
22206
+ }
22207
+ function getMessageOrderMs(message) {
22208
+ return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
22209
+ }
22210
+ function sortMessages(messages) {
22211
+ return [...messages].sort((a, b) => {
22212
+ const aMs = getMessageOrderMs(a);
22213
+ const bMs = getMessageOrderMs(b);
22214
+ if (aMs !== bMs) {
22215
+ return aMs - bMs;
22216
+ }
22217
+ return String(a?.id || "").localeCompare(String(b?.id || ""));
22218
+ });
22219
+ }
22220
+ function mergeMessages(...groups) {
22221
+ const merged = new Map;
22222
+ for (const group of groups) {
22223
+ for (const message of group || []) {
22224
+ const key = getMessageKey(message);
22225
+ if (!key) {
22226
+ continue;
22227
+ }
22228
+ merged.set(key, message);
22229
+ }
22230
+ }
22231
+ return sortMessages([...merged.values()]);
22232
+ }
22233
+ var init_state = __esm(() => {
22234
+ init_core();
22235
+ init_time();
22236
+ });
22237
+
22170
22238
  // ../../core/chat/ids.js
22171
22239
  function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
22172
22240
  const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
@@ -24905,6 +24973,24 @@ async function openStateRecordsWithEvidence(epochState, records) {
24905
24973
  closeStateEpoch(opened);
24906
24974
  }
24907
24975
  }
24976
+ async function readChatMemberState(cloud, epochState, actorChatPK) {
24977
+ if (!chatEpochCapabilities(epochState).sharedReceipts)
24978
+ return null;
24979
+ const member = epochState?.manifest?.members?.find((value) => value.chatPK === actorChatPK);
24980
+ if (!member)
24981
+ return null;
24982
+ const opened = openStateEpoch(epochState);
24983
+ try {
24984
+ const slotId = deriveChatMemberStateSlot(opened.epoch, member.chatSigningPK);
24985
+ const record = await cloud.chat.memberState.readSlot(opened.epoch.epochId, slotId);
24986
+ if (!record || !stateRecordActive(record))
24987
+ return null;
24988
+ const state = await openChatMemberState(opened.epoch, record);
24989
+ return state.actor === actorChatPK ? stateRecordProjection(opened.epoch.epochId, record, state) : null;
24990
+ } finally {
24991
+ closeStateEpoch(opened);
24992
+ }
24993
+ }
24908
24994
  function watchChatMemberStateEvidence(cloud, epochState, onUpdate, onError) {
24909
24995
  if (!chatEpochCapabilities(epochState).sharedReceipts)
24910
24996
  return () => {};
@@ -26092,8 +26178,6 @@ function cleanChatPayment(message, { requireTx }) {
26092
26178
  } catch {
26093
26179
  return null;
26094
26180
  }
26095
- if (message.mode || message.transfers?.length !== 1)
26096
- return null;
26097
26181
  } else if (message.tokenIdentifier != null)
26098
26182
  return null;
26099
26183
  const transfers = cleanChatPaymentTransfers(message?.transfers, { requireTx, token });
@@ -26105,6 +26189,13 @@ function cleanChatPayment(message, { requireTx }) {
26105
26189
  if (transfers.length > 1 && !mode || rawMode && !mode)
26106
26190
  return null;
26107
26191
  const total = transfers.reduce((sum, transfer) => sum + BigInt(transfer.a), 0n);
26192
+ if (token) {
26193
+ try {
26194
+ tokenUnits(total);
26195
+ } catch {
26196
+ return null;
26197
+ }
26198
+ }
26108
26199
  if (!mode && (transfers.length !== 1 || BigInt(transfers[0].a) !== amount))
26109
26200
  return null;
26110
26201
  if (mode === GROUP_SEND_MODES.EACH && transfers.some((transfer) => BigInt(transfer.a) !== amount))
@@ -26130,14 +26221,14 @@ function makeChatPaymentPayload({ amountSats, amountUnits, tokenIdentifier, tran
26130
26221
  if (token) {
26131
26222
  tokenAsset(tokenIdentifier, paymentNetwork);
26132
26223
  tokenUnits(amount);
26133
- if (mode || transfers.length !== 1)
26134
- throw new Error("token payments require one direct recipient");
26135
26224
  }
26136
26225
  const rawMode = cleanText(mode);
26137
26226
  const groupMode = GROUP_PAYMENT_MODES.has(rawMode) ? rawMode : null;
26138
26227
  if (transfers.length > 1 && !groupMode || rawMode && !groupMode)
26139
26228
  throw new Error("chat payment mode required");
26140
26229
  const total = transfers.reduce((sum, transfer) => sum + BigInt(transfer.a), 0n);
26230
+ if (token)
26231
+ tokenUnits(total);
26141
26232
  if (!groupMode && (transfers.length !== 1 || BigInt(transfers[0].a) !== amount)) {
26142
26233
  throw new Error("direct payment amount must match its transfer");
26143
26234
  }
@@ -26187,8 +26278,6 @@ function chatPaymentMembersValid(message, payment) {
26187
26278
  if (sender && recipients.includes(sender))
26188
26279
  return false;
26189
26280
  const epochMembers = Array.isArray(message?.epochMemberChatPKs) ? new Set(message.epochMemberChatPKs.map((member) => cleanText(member).toLowerCase()).filter(Boolean)) : null;
26190
- if (message.tokenIdentifier != null && epochMembers?.size > 2)
26191
- return false;
26192
26281
  return !epochMembers?.size || recipients.every((recipient) => epochMembers.has(recipient));
26193
26282
  }
26194
26283
  function isChatPayment(message) {
@@ -26796,7 +26885,6 @@ var init_control = __esm(() => {
26796
26885
  init_files2();
26797
26886
  init_text();
26798
26887
  init_tab2();
26799
- init_call();
26800
26888
  init_request();
26801
26889
  init_types();
26802
26890
  sourceGoneVisibleMessageCache = new WeakMap;
@@ -26912,17 +27000,10 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
26912
27000
  const heldExpired = [];
26913
27001
  const shorten = [];
26914
27002
  let nextExpiryAt = null;
26915
- const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
26916
- const endedCalls = new Set(projectedMessages.filter(isServerConfirmedMsg).filter((message) => readCallMessage(message)?.event === "ended").map((message) => message.callId));
26917
27003
  for (const message of projectedMessages) {
26918
27004
  if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
26919
27005
  continue;
26920
- const call = readCallMessage(message);
26921
- if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined && !endedCalls.has(call.callId) || options.activeCallId === call.callId))
26922
- continue;
26923
- const readTarget = isPaymentRequest(message) ? message.paymentConfirmation : message;
26924
- if (!readTarget)
26925
- continue;
27006
+ const readTarget = isPaymentRequest(message) && message.paymentConfirmation ? message.paymentConfirmation : message;
26926
27007
  const originalSeenAt = allReadersSeenAt(message, members, evidence, options.memberStatesByEpoch);
26927
27008
  const resultSeenAt = readTarget === message ? originalSeenAt : allReadersSeenAt(readTarget, members, evidence, options.memberStatesByEpoch);
26928
27009
  if (originalSeenAt == null || resultSeenAt == null)
@@ -26968,7 +27049,6 @@ var init_expiry = __esm(() => {
26968
27049
  init_control();
26969
27050
  init_receipt();
26970
27051
  init_config();
26971
- init_call();
26972
27052
  init_request();
26973
27053
  });
26974
27054
 
@@ -27118,7 +27198,6 @@ function sendableMsgPayload(message) {
27118
27198
  var init_compose = __esm(() => {
27119
27199
  init_control();
27120
27200
  init_messagekeys();
27121
- init_call();
27122
27201
  });
27123
27202
 
27124
27203
  // ../../core/chat/messages/preview.js
@@ -27470,7 +27549,6 @@ var init_preview = __esm(() => {
27470
27549
  init_files2();
27471
27550
  init_text();
27472
27551
  init_request();
27473
- init_call();
27474
27552
  CHAT_PREVIEW_TEXT = Object.freeze({
27475
27553
  hidden: "",
27476
27554
  fallback: "sent a message",
@@ -27497,7 +27575,6 @@ var init_messages = __esm(() => {
27497
27575
  init_preview();
27498
27576
  init_request();
27499
27577
  init_tab2();
27500
- init_call();
27501
27578
  "use client";
27502
27579
  });
27503
27580
 
@@ -31159,6 +31236,20 @@ function uniqueValues(items) {
31159
31236
  function sortedUniqueValues(items) {
31160
31237
  return uniqueValues(items).sort();
31161
31238
  }
31239
+ function sameArray(a, b) {
31240
+ if (a === b) {
31241
+ return true;
31242
+ }
31243
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
31244
+ return false;
31245
+ }
31246
+ for (let index = 0;index < a.length; index += 1) {
31247
+ if (a[index] !== b[index]) {
31248
+ return false;
31249
+ }
31250
+ }
31251
+ return true;
31252
+ }
31162
31253
 
31163
31254
  // ../../core/chat/chats.js
31164
31255
  function sameSigningKeys(left, right) {
@@ -32682,7 +32773,8 @@ function writeCachedChats(cache, chats, options = {}) {
32682
32773
  }
32683
32774
  }
32684
32775
  for (const item of items) {
32685
- byId.set(item.id, item);
32776
+ const previous = reviveChat(payload.chatsById?.[item.id]);
32777
+ byId.set(item.id, previous ? serializeChat(mergeChatActivity(item, previous, options.chatPK)) : item);
32686
32778
  }
32687
32779
  const merged = [...byId.values()];
32688
32780
  const capped = merged.length > LOCAL_CHAT_CACHE_MAX_ITEMS;
@@ -32858,18 +32950,24 @@ function readCachedReadFrontiers(cache, chatId) {
32858
32950
  return [];
32859
32951
  return Object.values(source).map(cleanFrontier2).filter(Boolean);
32860
32952
  }
32861
- function writeCachedReadFrontier(cache, chatId, value) {
32953
+ function writeCachedChatRead(cache, chatId, { frontier: value, message, openedAt, chatPK } = {}) {
32862
32954
  const frontier = cleanFrontier2(value);
32863
- if (!cache?.patch || !chatId || !frontier)
32955
+ if (!cache?.patch || !chatId || !frontier && !message)
32864
32956
  return Promise.resolve(false);
32865
32957
  return cache.patch((payload) => {
32866
- const chats = payload.pendingReadFrontiersByChat || {};
32867
- const current = chats[chatId] || {};
32868
- chats[chatId] = {
32869
- ...current,
32870
- [frontier.epochId]: laterFrontier(cleanFrontier2(current[frontier.epochId]), frontier)
32871
- };
32872
- payload.pendingReadFrontiersByChat = chats;
32958
+ if (frontier) {
32959
+ const chats = payload.pendingReadFrontiersByChat || {};
32960
+ const current = chats[chatId] || {};
32961
+ chats[chatId] = {
32962
+ ...current,
32963
+ [frontier.epochId]: laterFrontier(cleanFrontier2(current[frontier.epochId]), frontier)
32964
+ };
32965
+ payload.pendingReadFrontiersByChat = chats;
32966
+ }
32967
+ const chat = payload.chatsById?.[chatId];
32968
+ if (chat && message) {
32969
+ [payload.chatsById[chatId]] = markChatsRead([chat], chatId, message, { openedAt, chatPK });
32970
+ }
32873
32971
  return payload;
32874
32972
  }, { flush: true }).then(() => true);
32875
32973
  }
@@ -32894,6 +32992,7 @@ function dropCachedReadFrontier(cache, chatId, value) {
32894
32992
  }, { flush: true }).then(() => true);
32895
32993
  }
32896
32994
  var init_reads = __esm(() => {
32995
+ init_read();
32897
32996
  "use client";
32898
32997
  });
32899
32998
 
@@ -33847,6 +33946,15 @@ function createPendingSendQueue({ diag = null } = {}) {
33847
33946
  const generationRef = { current: 0 };
33848
33947
  const sentAtRef = { current: [] };
33849
33948
  const timerRef = { current: null };
33949
+ let active = true;
33950
+ let scheduleVersion = 0;
33951
+ function clearSchedule() {
33952
+ scheduleVersion += 1;
33953
+ if (timerRef.current !== null)
33954
+ clearTimeout(timerRef.current);
33955
+ timerRef.current = null;
33956
+ scheduledRef.current = false;
33957
+ }
33850
33958
  function nextRateDelay() {
33851
33959
  const now = Date.now();
33852
33960
  const since = now - CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS;
@@ -33888,6 +33996,8 @@ function createPendingSendQueue({ diag = null } = {}) {
33888
33996
  }
33889
33997
  }
33890
33998
  const flush = () => {
33999
+ if (!active)
34000
+ return;
33891
34001
  if (scheduledRef.current || !queueRef.current.length) {
33892
34002
  markDiag(diag, "chat.send.queue.flush.skip", {
33893
34003
  scheduled: !!scheduledRef.current,
@@ -33913,7 +34023,10 @@ function createPendingSendQueue({ diag = null } = {}) {
33913
34023
  running: runningJobsRef.current.size,
33914
34024
  sentWindow: sentAtRef.current.length
33915
34025
  });
34026
+ const version = ++scheduleVersion;
33916
34027
  timerRef.current = setTimeout(() => {
34028
+ if (!active || version !== scheduleVersion)
34029
+ return;
33917
34030
  timerRef.current = null;
33918
34031
  scheduledRef.current = false;
33919
34032
  const nextIndex = nextRunnableJobIndex();
@@ -34008,6 +34121,7 @@ function createPendingSendQueue({ diag = null } = {}) {
34008
34121
  };
34009
34122
  const resetPendingSendQueue = () => {
34010
34123
  generationRef.current += 1;
34124
+ clearSchedule();
34011
34125
  if (queueRef.current.length) {
34012
34126
  const error = new Error("chat reset");
34013
34127
  queueRef.current.forEach((job) => {
@@ -34016,16 +34130,18 @@ function createPendingSendQueue({ diag = null } = {}) {
34016
34130
  });
34017
34131
  }
34018
34132
  queueRef.current = [];
34019
- scheduledRef.current = false;
34020
34133
  sentAtRef.current = [];
34021
- if (timerRef.current) {
34022
- clearTimeout(timerRef.current);
34023
- timerRef.current = null;
34024
- }
34025
34134
  };
34026
34135
  return {
34027
34136
  enqueuePendingSendJob,
34028
34137
  resetPendingSendQueue,
34138
+ setActive(value) {
34139
+ active = value === true;
34140
+ clearSchedule();
34141
+ markDiag(diag, "chat.send.queue.activity", { active, queued: queueRef.current.length, running: runningJobsRef.current.size });
34142
+ if (active)
34143
+ flush();
34144
+ },
34029
34145
  close: resetPendingSendQueue
34030
34146
  };
34031
34147
  }
@@ -35736,6 +35852,7 @@ ${cid}` : "";
35736
35852
  };
35737
35853
  return {
35738
35854
  resetSending,
35855
+ setActive: pendingSendQueueRef.current.setActive,
35739
35856
  ackMessages,
35740
35857
  stageMessage: showLocalMessage,
35741
35858
  discardLocalMessage: (chatId, cid) => ackMessages(chatId, [cid], { remove: true }),
@@ -38443,7 +38560,7 @@ function createChatListCache({ getSources, state }) {
38443
38560
  const pending = pendingWrite;
38444
38561
  pendingWrite = null;
38445
38562
  if (pending?.cache && Array.isArray(pending.chats)) {
38446
- writeCachedChats(pending.cache, pending.chats, { ownerListToken: pending.ownerListToken });
38563
+ writeCachedChats(pending.cache, pending.chats, { ownerListToken: pending.ownerListToken, chatPK: pending.chatPK });
38447
38564
  }
38448
38565
  };
38449
38566
  const queue = (nextChats, options = {}) => {
@@ -38455,6 +38572,7 @@ function createChatListCache({ getSources, state }) {
38455
38572
  const requestedToken = requestedCoverage === CACHE_COVERAGE.COMPLETE ? cleanOwnerListToken(options.ownerListToken) : requestedCoverage === CACHE_COVERAGE.INCOMPLETE ? null : pendingWrite?.ownerListToken;
38456
38573
  pendingWrite = {
38457
38574
  cache: sources.localCache,
38575
+ chatPK: sources.chatPK,
38458
38576
  chats: nextChats,
38459
38577
  ownerListToken: requestedToken
38460
38578
  };
@@ -38481,6 +38599,11 @@ function createChatListCache({ getSources, state }) {
38481
38599
  state.setHasMoreChats(false);
38482
38600
  }
38483
38601
  hydrateReadCache(cachedChats, sources.readCacheRef.current);
38602
+ for (const chatId of Object.keys(cachedPayload?.pendingReadFrontiersByChat || {})) {
38603
+ const at = Math.max(0, ...readCachedReadFrontiers(sources.localCache, chatId).map((frontier) => frontier.at));
38604
+ if (at > (sources.readCacheRef.current.get(chatId) || 0))
38605
+ sources.readCacheRef.current.set(chatId, at);
38606
+ }
38484
38607
  const cachedChatsWithRead = applyReadCache(cachedChats, sources.chatPK, sources.readCacheRef.current);
38485
38608
  if (hasSavedChatSnapshot) {
38486
38609
  const shownCachedChats = hydrateRows(cachedChatsWithRead);
@@ -41315,6 +41438,14 @@ var init_listepoch = __esm(() => {
41315
41438
  });
41316
41439
 
41317
41440
  // ../../core/chat/listindex.js
41441
+ function sameRecord(left, right) {
41442
+ if (left === right)
41443
+ return true;
41444
+ if (!left || !right)
41445
+ return false;
41446
+ const keys = Object.keys(left);
41447
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && Object.is(left[key], right[key]));
41448
+ }
41318
41449
  function ownerRevision(chat) {
41319
41450
  return Number.isSafeInteger(chat?.ownEntry?.ownerRevision) ? chat.ownEntry.ownerRevision : 0;
41320
41451
  }
@@ -41344,11 +41475,26 @@ function activityFromChat(chat) {
41344
41475
  unseen: chat.unseen === true
41345
41476
  };
41346
41477
  }
41347
- function structuralChat(chat) {
41478
+ function structuralChat(chat, previous) {
41348
41479
  if (!chat)
41349
41480
  return null;
41481
+ const structure = {};
41482
+ if (previous && previous.epochId === chat.epochId && previous.epochVersion === chat.epochVersion) {
41483
+ for (const key of ["memberChatPKs", "memberUids"]) {
41484
+ if (sameArray(previous[key], chat[key]))
41485
+ structure[key] = previous[key];
41486
+ }
41487
+ for (const key of ["signingKeysByChatKey", "settings"]) {
41488
+ if (sameRecord(previous[key], chat[key]))
41489
+ structure[key] = previous[key];
41490
+ }
41491
+ if (Array.isArray(previous.members) && Array.isArray(chat.members) && previous.members.length === chat.members.length && previous.members.every((member, index) => sameRecord(member, chat.members[index]))) {
41492
+ structure.members = previous.members;
41493
+ }
41494
+ }
41350
41495
  return {
41351
41496
  ...chat,
41497
+ ...structure,
41352
41498
  preview: null,
41353
41499
  readMs: null,
41354
41500
  inboxMessageId: null,
@@ -41412,7 +41558,7 @@ function sourceEntry() {
41412
41558
  presentation: null
41413
41559
  };
41414
41560
  }
41415
- function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenChatId, state }) {
41561
+ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenChatId, state, onOwnersChanged }) {
41416
41562
  const entries = new Map;
41417
41563
  const authorityBoundaries = new Map;
41418
41564
  let authorityRevision = 0;
@@ -41533,8 +41679,14 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41533
41679
  const visible = sources.blockedReady ? filterBlockedChats(composedRows()) : [];
41534
41680
  const trimmed = trimExpiredChatPreviews(visible);
41535
41681
  const shown = sources.blockedReady ? sortedChats(filterBlockedChats(setLocalChats(trimmed, sources.localByChatRef.current))) : [];
41536
- sources.chatsRef.current = shown;
41537
- state.setChats((current) => sameChats(current, shown) ? current : shown);
41682
+ state.setChats((current) => {
41683
+ const byId = new Map(current.map((chat) => [chat.id, chat]));
41684
+ const next = shown.map((chat) => {
41685
+ const previous = byId.get(chat.id);
41686
+ return previous && sameChats([previous], [chat]) ? previous : chat;
41687
+ });
41688
+ return sameArray(current, next) ? current : next;
41689
+ });
41538
41690
  const last = getLastChat(shown);
41539
41691
  state.setLastChat((current) => sameLastChat(current, last) ? current : last);
41540
41692
  updatePeers(shown);
@@ -41552,6 +41704,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41552
41704
  if (options.warm !== false) {
41553
41705
  sources.warmChats(sources.blockedReady ? filterBlockedChats(owners) : []);
41554
41706
  }
41707
+ onOwnersChanged?.(owners);
41555
41708
  return owners;
41556
41709
  };
41557
41710
  const applyBlockedFilter = () => {
@@ -41577,7 +41730,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41577
41730
  for (const chat of filterHiddenChats(rows)) {
41578
41731
  const entry = entryFor(chat.id);
41579
41732
  captureActivity(entry, chat);
41580
- entry.cache = structuralChat(chat);
41733
+ entry.cache = structuralChat(chat, entry.cache);
41581
41734
  }
41582
41735
  const shown = render();
41583
41736
  updatePeers(shown);
@@ -41627,7 +41780,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41627
41780
  for (const chat of incoming) {
41628
41781
  const entry = entryFor(chat.id);
41629
41782
  captureActivity(entry, chat);
41630
- entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat));
41783
+ entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat, structuralOwnerFor(entry) || entry.cache));
41631
41784
  if (entry.committedOwner && compareOwners(entry.observedOwner, entry.committedOwner) >= 0) {
41632
41785
  entry.committedOwner = null;
41633
41786
  }
@@ -41645,7 +41798,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41645
41798
  for (const chat of (rows || []).filter((item) => item?.id && sourceCanPublish(item, options.sourceRevision))) {
41646
41799
  const entry = entryFor(chat.id);
41647
41800
  captureActivity(entry, chat);
41648
- entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat));
41801
+ entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat, structuralOwnerFor(entry) || entry.cache));
41649
41802
  if (entry.committedOwner && compareOwners(entry.observedOwner, entry.committedOwner) >= 0) {
41650
41803
  entry.committedOwner = null;
41651
41804
  }
@@ -41665,7 +41818,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41665
41818
  return false;
41666
41819
  const entry = entryFor(chat.id);
41667
41820
  captureActivity(entry, chat);
41668
- entry.committedOwner = selectOwner(entry.committedOwner, structuralChat(chat));
41821
+ entry.committedOwner = selectOwner(entry.committedOwner, structuralChat(chat, structuralOwnerFor(entry) || entry.cache));
41669
41822
  if (ownerCoversAnnouncement(ownerFor(entry), entry.announcement))
41670
41823
  entry.announcement = null;
41671
41824
  entry.cache = null;
@@ -41841,6 +41994,9 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
41841
41994
  });
41842
41995
  if (next === current)
41843
41996
  return false;
41997
+ const readCache = getSources().readCacheRef.current;
41998
+ if (next.readMs > (readCache.get(chatId) || 0))
41999
+ readCache.set(chatId, next.readMs);
41844
42000
  replaceActivity(entry, next);
41845
42001
  publishOwners();
41846
42002
  return true;
@@ -42520,6 +42676,79 @@ var init_listpreview = __esm(() => {
42520
42676
  init_chats();
42521
42677
  });
42522
42678
 
42679
+ // ../../core/chat/listreads.js
42680
+ function createChatListReads({ getSources, index }) {
42681
+ const attempted = new Set;
42682
+ let queue = [];
42683
+ let generation = 0;
42684
+ let running = false;
42685
+ const admitted = (chat, sources) => !sources.chatBanned && !(chat?.memberUids || []).some((uid) => sources.blockedUidSet.has(uid)) && chat?.unseen && chat.ownEntry && !isChatMembershipRemoved(chat);
42686
+ const eligible = (chat, sources) => sources.isActive && admitted(chat, sources);
42687
+ const pump = async () => {
42688
+ if (running)
42689
+ return;
42690
+ running = true;
42691
+ const run2 = generation;
42692
+ try {
42693
+ while (queue.length && run2 === generation) {
42694
+ const { chatId, epochId, key } = queue.shift();
42695
+ const sources = getSources();
42696
+ const chat = index.getOwnerChat(chatId);
42697
+ if (!eligible(chat, sources)) {
42698
+ attempted.delete(key);
42699
+ continue;
42700
+ }
42701
+ try {
42702
+ const epoch = epochId === chat.epochId ? chat.epochState : await sources.getEpochState(chat, epochId);
42703
+ if (run2 !== generation)
42704
+ return;
42705
+ const state = epoch ? await readChatMemberState(sources.cloud, epoch, sources.chatPK) : null;
42706
+ if (run2 !== generation || !admitted(index.getOwnerChat(chatId), getSources()))
42707
+ continue;
42708
+ const frontier = state?.readFrontier;
42709
+ if (frontier)
42710
+ index.markChatRead(chatId, { id: frontier.id, ts: frontier.at }, frontier.seenAt);
42711
+ } catch (error) {
42712
+ if (run2 !== generation)
42713
+ return;
42714
+ markError(sources.diag, "chat.list.read.restore", Date.now(), error);
42715
+ }
42716
+ }
42717
+ } finally {
42718
+ if (run2 === generation)
42719
+ running = false;
42720
+ }
42721
+ };
42722
+ return {
42723
+ restore(chats) {
42724
+ const sources = getSources();
42725
+ if (typeof sources.cloud?.chat?.memberState?.readSlot !== "function")
42726
+ return;
42727
+ for (const chat of chats) {
42728
+ if (!eligible(chat, sources))
42729
+ continue;
42730
+ const epochId = chat.preview?.epochId || chat.epochId;
42731
+ const key = `${chat.id}:${epochId}`;
42732
+ if (attempted.has(key))
42733
+ continue;
42734
+ attempted.add(key);
42735
+ queue.push({ chatId: chat.id, epochId, key });
42736
+ }
42737
+ return pump();
42738
+ },
42739
+ reset() {
42740
+ generation += 1;
42741
+ attempted.clear();
42742
+ queue = [];
42743
+ running = false;
42744
+ }
42745
+ };
42746
+ }
42747
+ var init_listreads = __esm(() => {
42748
+ init_memberstate();
42749
+ init_entry();
42750
+ });
42751
+
42523
42752
  // ../../core/chat/liststate.js
42524
42753
  function nextValue(current, next) {
42525
42754
  return typeof next === "function" ? next(current) : next;
@@ -42698,7 +42927,16 @@ function createChatList(initialSources) {
42698
42927
  const filterHiddenChats = (chats) => (chats || []).filter((chatItem) => chatItem?.id && !isHiddenChatId(chatItem.id));
42699
42928
  const state = createChatListState({ getSources });
42700
42929
  const cache = createChatListCache({ getSources, state });
42701
- const index = createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenChatId, state });
42930
+ let reads = null;
42931
+ const index = createChatListIndex({
42932
+ cache,
42933
+ filterHiddenChats,
42934
+ getSources,
42935
+ isHiddenChatId,
42936
+ state,
42937
+ onOwnersChanged: (chats) => reads?.restore(chats)
42938
+ });
42939
+ reads = createChatListReads({ getSources, index });
42702
42940
  const epoch = createChatListEpochOwner({ getGeneration, getSources, index });
42703
42941
  const paging = createChatListPaging({ cache, getGeneration, getSources, index, isHiddenChatId, state });
42704
42942
  const invalidateRuntimeTasks = () => {
@@ -42707,6 +42945,7 @@ function createChatList(initialSources) {
42707
42945
  epoch.reset();
42708
42946
  };
42709
42947
  const resetChatList = (ready = false) => {
42948
+ reads.reset();
42710
42949
  invalidateRuntimeTasks();
42711
42950
  index.reset(ready);
42712
42951
  };
@@ -42757,6 +42996,7 @@ function createChatList(initialSources) {
42757
42996
  index.applyBlockedFilter();
42758
42997
  paging.fillVisibleChatPage();
42759
42998
  }
42999
+ reads.restore(index.getOwnerChats());
42760
43000
  };
42761
43001
  const start2 = () => {
42762
43002
  if (listener.isListening())
@@ -42765,6 +43005,7 @@ function createChatList(initialSources) {
42765
43005
  preview2.schedule();
42766
43006
  };
42767
43007
  const close = () => {
43008
+ reads.reset();
42768
43009
  listener.close();
42769
43010
  preview2.close();
42770
43011
  cache.flush();
@@ -42810,6 +43051,7 @@ var init_listsession = __esm(() => {
42810
43051
  init_listlistener();
42811
43052
  init_listpaging();
42812
43053
  init_listpreview();
43054
+ init_listreads();
42813
43055
  });
42814
43056
 
42815
43057
  // ../../core/chat/inboxtransport.js
@@ -45192,7 +45434,7 @@ function chatDisplayTitle(chat, selfChatPK, peerByChatPK, options = {}) {
45192
45434
  }));
45193
45435
  if (chat?.lineage === "direct" && names.length === 1)
45194
45436
  return names[0];
45195
- const shown = names.slice(0, 3);
45437
+ const shown = names.slice(0, options.maxNames ?? 3);
45196
45438
  const extra = names.length - shown.length;
45197
45439
  return `${shown.join(", ")}${extra > 0 ? ` +${extra}` : ""}`;
45198
45440
  }
@@ -45854,9 +46096,6 @@ function createChatSessionPending({
45854
46096
  return track(launchId, previousOperation.then((result) => sendDurableSharedAttachment(result?.chatId || launchId, queuedAttachment, options)));
45855
46097
  };
45856
46098
  const sendChatMessage = (chatId, message, options = {}) => {
45857
- const chat = getOwnerChat(resolvePendingChatId(chatId)) || launchForChat(chatId)?.chat;
45858
- if (message?.tokenIdentifier != null && chat?.lineage === "group")
45859
- throw new Error("group payments support bitcoin only");
45860
46099
  const operation = operationForChat(chatId);
45861
46100
  if (operation?.discarding)
45862
46101
  throw makeChatUnavailableError();
@@ -47314,7 +47553,6 @@ function createChatSeen({
47314
47553
  const scheduleRead = (chatId, message, previewMs, frontierMessage, messages = []) => {
47315
47554
  const frontier = Number.isSafeInteger(frontierMessage?.at) && frontierMessage?.epochId ? frontierMessage : readFrontierFromMessage(frontierMessage, { seenAt: previewMs });
47316
47555
  if (frontier) {
47317
- writeCachedReadFrontier(localCache, chatId, frontier);
47318
47556
  onReadFrontier?.(chatId, frontier);
47319
47557
  }
47320
47558
  const timing = getReadWriteTiming?.(chatId) || {};
@@ -47348,6 +47586,12 @@ function createChatSeen({
47348
47586
  const frontier = readFrontierFromMessage(frontierMessage, { seenAt: read?.previewMs || Date.now() });
47349
47587
  if (!read && !frontier && !cachedFrontiers.length)
47350
47588
  return false;
47589
+ const savedRead = writeCachedChatRead(localCache, chatId, {
47590
+ frontier,
47591
+ message: read?.preview,
47592
+ openedAt: read?.previewMs,
47593
+ chatPK
47594
+ }).catch((error) => markError(diag, "chat.read.cache", Date.now(), error));
47351
47595
  if (read) {
47352
47596
  markChatRead?.(chatId, read.preview, read.previewMs);
47353
47597
  readCacheRef.current.set(chatId, read.readMs);
@@ -47357,6 +47601,7 @@ function createChatSeen({
47357
47601
  for (const cached of cachedFrontiers)
47358
47602
  scheduleRead(chatId, preview2, previewMs, cached, messages);
47359
47603
  scheduleRead(chatId, preview2, previewMs, frontierMessage, messages);
47604
+ await savedRead;
47360
47605
  return true;
47361
47606
  };
47362
47607
  const markChatReadState = async (chatId, message, options = {}) => {
@@ -47457,6 +47702,7 @@ function createChatSessionActions({
47457
47702
  diag,
47458
47703
  readWriteInterval,
47459
47704
  adoptLocalMessageMedia,
47705
+ getActive,
47460
47706
  getIdentity,
47461
47707
  getLiveActivity,
47462
47708
  getOwnerChat,
@@ -47588,6 +47834,7 @@ function createChatSessionActions({
47588
47834
  reconcileEpoch: (chatId) => getChatListOwner().reconcileChatEpoch(chatId),
47589
47835
  diag
47590
47836
  });
47837
+ sendActions.setActive(getActive());
47591
47838
  reactionActions = createChatReaction({
47592
47839
  cloud,
47593
47840
  uid: identity().uid,
@@ -47741,10 +47988,10 @@ function sameReadFrontier(left, right) {
47741
47988
  return left.actor === right.actor && left.readFrontier?.epochId === right.readFrontier?.epochId && left.readFrontier?.id === right.readFrontier?.id && left.readFrontier?.at === right.readFrontier?.at && left.readFrontier?.seenAt === right.readFrontier?.seenAt;
47742
47989
  }
47743
47990
  function sameActivity2(left, right) {
47744
- return left.connected === right.connected && left.activeChatPKs.length === right.activeChatPKs.length && left.compositions.length === right.compositions.length && left.readFrontiers.length === right.readFrontiers.length && left.activeChatPKs.every((value, index) => value === right.activeChatPKs[index]) && left.compositions.every((value, index) => sameComposition(value, right.compositions[index])) && left.readFrontiers.every((value, index) => sameReadFrontier(value, right.readFrontiers[index]));
47991
+ return left.connected === right.connected && left.lastObservedAtByChatPK === right.lastObservedAtByChatPK && left.activeChatPKs.length === right.activeChatPKs.length && left.compositions.length === right.compositions.length && left.readFrontiers.length === right.readFrontiers.length && left.activeChatPKs.every((value, index) => value === right.activeChatPKs[index]) && left.compositions.every((value, index) => sameComposition(value, right.compositions[index])) && left.readFrontiers.every((value, index) => sameReadFrontier(value, right.readFrontiers[index]));
47745
47992
  }
47746
47993
  function hasActivity(activity) {
47747
- return !!(activity?.connected || activity?.activeChatPKs?.length || activity?.compositions?.length || activity?.readFrontiers?.length);
47994
+ return !!(activity?.connected || activity?.activeChatPKs?.length || activity?.compositions?.length || activity?.readFrontiers?.length || Object.keys(activity?.lastObservedAtByChatPK || {}).length);
47748
47995
  }
47749
47996
  function summarizeLocalMessage(summary, message) {
47750
47997
  summary.messages += 1;
@@ -47885,6 +48132,17 @@ function createChatSessionState({
47885
48132
  return;
47886
48133
  const next = new Map(liveActivityByChat);
47887
48134
  const previous = next.get(chatId);
48135
+ let lastObservedAtByChatPK = previous?.lastObservedAtByChatPK;
48136
+ const departed = previous?.connected ? previous.activeChatPKs.filter((actor) => !activity?.connected || !activity.activeChatPKs.includes(actor)) : [];
48137
+ if (departed.length) {
48138
+ const now = Date.now();
48139
+ lastObservedAtByChatPK = { ...lastObservedAtByChatPK };
48140
+ for (const actor of departed)
48141
+ lastObservedAtByChatPK[actor] = Math.max(lastObservedAtByChatPK[actor] || 0, now);
48142
+ Object.freeze(lastObservedAtByChatPK);
48143
+ }
48144
+ if (lastObservedAtByChatPK)
48145
+ activity = { ...activity, lastObservedAtByChatPK };
47888
48146
  if (previous?.connected && !activity?.connected)
47889
48147
  onLiveDisconnect(chatId);
47890
48148
  if (hasActivity(activity)) {
@@ -48298,6 +48556,7 @@ function normalizeChatSessionSources(sources = {}) {
48298
48556
  uid: sources.uid || "",
48299
48557
  online: sources.online !== false,
48300
48558
  blocked: Array.isArray(sources.blocked) ? sources.blocked : [],
48559
+ blockedPending: Array.isArray(sources.blockedPending) ? sources.blockedPending : [],
48301
48560
  blockedReady: sources.blockedReady === true,
48302
48561
  chatPK: sources.chatPK || "",
48303
48562
  chatBanned: sources.chatBanned === true,
@@ -48347,7 +48606,6 @@ function createChatSession({
48347
48606
  mls = null,
48348
48607
  live = null,
48349
48608
  maintenance,
48350
- resolveActiveCall,
48351
48609
  incomingChatDecision = null,
48352
48610
  getPeerProfile = null,
48353
48611
  refreshPeerProfile = null,
@@ -48365,6 +48623,7 @@ function createChatSession({
48365
48623
  uid = "",
48366
48624
  online = true,
48367
48625
  blocked = [],
48626
+ blockedPending = [],
48368
48627
  blockedReady = false,
48369
48628
  chatPK = "",
48370
48629
  chatSigningPK = "",
@@ -48381,6 +48640,7 @@ function createChatSession({
48381
48640
  let inboxEnabled = initialSources.inboxEnabled === true;
48382
48641
  let started = false;
48383
48642
  let appStateSubscription = null;
48643
+ const messageListeners = new Set;
48384
48644
  let mlsPoolReplenishment = null;
48385
48645
  let mlsPoolReplenishQueued = false;
48386
48646
  let mlsPoolNeedsReplenish = true;
@@ -48555,6 +48815,13 @@ function createChatSession({
48555
48815
  };
48556
48816
  const handleBatchMessages = (chatId, messages) => {
48557
48817
  chatListOwner.observeMessages(chatId, messages);
48818
+ for (const listener of messageListeners) {
48819
+ try {
48820
+ listener(chatId, messages);
48821
+ } catch (error) {
48822
+ diag?.("chat.message.observer.error", { code: error?.code || "chat/observer" });
48823
+ }
48824
+ }
48558
48825
  if (![...messages || []].reverse().some(isEpochTransitionMsg))
48559
48826
  return;
48560
48827
  const startedAt = Date.now();
@@ -48987,8 +49254,6 @@ function createChatSession({
48987
49254
  const chat = getOwnerChat(chatId);
48988
49255
  if (!chat)
48989
49256
  throw makeChatUnavailableError();
48990
- if (payment.tokenIdentifier != null && chat.lineage !== "direct")
48991
- throw new Error("group payments support bitcoin only");
48992
49257
  const members = Array.isArray(chat?.memberChatPKs) ? chat.memberChatPKs.map((memberChatPK) => cleanText(memberChatPK).toLowerCase()) : (chat?.members || []).map((member) => cleanText(member?.chatPK).toLowerCase());
48993
49258
  const message = pending ? makePendingChatPayment(payment) : makeChatPayment(payment);
48994
49259
  const recipients = chatPaymentRecipients(message);
@@ -49015,6 +49280,7 @@ function createChatSession({
49015
49280
  };
49016
49281
  const getLocalMessages = (chatId) => isChatPendingDelete(chatId) || !isChatIdVisible(chatId) ? EMPTY_LOCAL_MESSAGES : localByChatRef.current.get(chatId) ?? EMPTY_LOCAL_MESSAGES;
49017
49282
  const blockedAuthority = (values) => [...values || []].sort().join("|");
49283
+ const committedBlocked = () => blocked.filter((uid2) => !blockedPending.includes(uid2));
49018
49284
  const bulkOperation = (blockedUids = null) => ({
49019
49285
  generation: authGeneration,
49020
49286
  identity: {
@@ -49030,7 +49296,7 @@ function createChatSession({
49030
49296
  deleteActions: actionOwner.deleteActions,
49031
49297
  blockedKey: blockedUids ? blockedAuthority(blockedUids) : null
49032
49298
  });
49033
- const isBulkOperationCurrent = (operation) => operation.generation === authGeneration && operation.deleteActions === actionOwner.deleteActions && (operation.blockedKey == null || operation.blockedKey === blockedAuthority(blocked));
49299
+ const isBulkOperationCurrent = (operation) => operation.generation === authGeneration && operation.deleteActions === actionOwner.deleteActions && (operation.blockedKey == null || operation.blockedKey === blockedAuthority(committedBlocked()));
49034
49300
  const assertBulkOperationCurrent = (operation) => {
49035
49301
  requireOnline();
49036
49302
  if (!isBulkOperationCurrent(operation))
@@ -49044,32 +49310,42 @@ function createChatSession({
49044
49310
  return 0;
49045
49311
  let allChats = await loadOwnerChats(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey);
49046
49312
  assertBulkOperationCurrent(operation);
49047
- let processedInbox = true;
49048
- while (processedInbox) {
49049
- processedInbox = await processInbox(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey, {
49050
- currentChats: allChats,
49051
- chatSigningPK: identity.chatSigningPK,
49052
- chatSigningSecret: identity.chatSigningSecret,
49053
- notificationPK: identity.notificationPK,
49054
- notificationPrivateKey: identity.notificationPrivateKey,
49055
- mls,
49056
- blockedUids: new Set(identity.blocked),
49057
- chatAdmission,
49058
- incomingChatDecision,
49059
- scanAllSlots: true,
49060
- requireComplete: true
49061
- });
49062
- assertBulkOperationCurrent(operation);
49063
- if (processedInbox) {
49064
- allChats = await loadOwnerChats(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey);
49065
- assertBulkOperationCurrent(operation);
49066
- }
49067
- }
49313
+ await processInbox(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey, {
49314
+ currentChats: allChats,
49315
+ chatSigningPK: identity.chatSigningPK,
49316
+ chatSigningSecret: identity.chatSigningSecret,
49317
+ notificationPK: identity.notificationPK,
49318
+ notificationPrivateKey: identity.notificationPrivateKey,
49319
+ mls,
49320
+ blockedUids: new Set(identity.blocked),
49321
+ chatAdmission,
49322
+ incomingChatDecision,
49323
+ scanAllSlots: true,
49324
+ requireComplete: true,
49325
+ isCurrent: () => isBulkOperationCurrent(operation),
49326
+ diag
49327
+ });
49328
+ assertBulkOperationCurrent(operation);
49329
+ allChats = await loadOwnerChats(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey);
49330
+ assertBulkOperationCurrent(operation);
49068
49331
  if (!allChats.length)
49069
49332
  return 0;
49070
49333
  let retired = 0;
49071
- for (const chat of allChats) {
49334
+ for (const ownedChat of allChats) {
49072
49335
  assertBulkOperationCurrent(operation);
49336
+ let chat = ownedChat;
49337
+ if (!isChatMembershipRemoved(chat)) {
49338
+ chatListOwner.commitOwner(chat);
49339
+ const reconciliation = await chatListOwner.reconcileChatEpoch(chat.id);
49340
+ assertBulkOperationCurrent(operation);
49341
+ chat = getOwnerChat(chat.id);
49342
+ if (!chat) {
49343
+ if (!reconciliation.removed)
49344
+ throw makeChatUnavailableError();
49345
+ retired += 1;
49346
+ continue;
49347
+ }
49348
+ }
49073
49349
  if (isChatMembershipRemoved(chat)) {
49074
49350
  await operation.deleteActions.deleteChat(chat, { cleanup: false });
49075
49351
  assertBulkOperationCurrent(operation);
@@ -49211,9 +49487,10 @@ function createChatSession({
49211
49487
  return false;
49212
49488
  }
49213
49489
  };
49214
- const prepareCall = async (chatId) => {
49490
+ const prepareCall = async (chatId, options = {}) => {
49215
49491
  const generation = authGeneration;
49216
- const current = () => generation === authGeneration && online && pendingOwner.canSendToChat(chatId);
49492
+ await sendReadiness.wait(chatId, options);
49493
+ const current = () => generation === authGeneration && !options.signal?.aborted && online && pendingOwner.canSendToChat(chatId);
49217
49494
  if (!current())
49218
49495
  throw new Error("chat unavailable");
49219
49496
  await pendingOwner.assertChatAdmission(chatId);
@@ -49230,7 +49507,13 @@ function createChatSession({
49230
49507
  throw new Error("chat unavailable");
49231
49508
  return chat.epochState;
49232
49509
  };
49233
- const materializeChat = (chatId) => pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
49510
+ const materializeChat = async (chatId, options = {}) => {
49511
+ const generation = authGeneration;
49512
+ await sendReadiness.wait(chatId, options);
49513
+ if (generation !== authGeneration || options.signal?.aborted)
49514
+ throw makeChatUnavailableError();
49515
+ return pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
49516
+ };
49234
49517
  const flushChatReadFrontier = (chatId) => {
49235
49518
  if (!online)
49236
49519
  return false;
@@ -49422,6 +49705,7 @@ function createChatSession({
49422
49705
  lastHydratedCacheKeyRef,
49423
49706
  chatsRef,
49424
49707
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
49708
+ getEpochState: (...args) => historyOwner.getEpochState(...args),
49425
49709
  warmChats: (...args) => messageBatchOwner.warm(...args),
49426
49710
  onOwnerRemoved: (chatIds) => {
49427
49711
  for (const chatId of chatIds)
@@ -49480,6 +49764,7 @@ function createChatSession({
49480
49764
  });
49481
49765
  };
49482
49766
  stateOwner.setActions({
49767
+ ensureChat,
49483
49768
  loadMoreChats: loadMoreChats2,
49484
49769
  selectChat,
49485
49770
  resolveChatRequest,
@@ -49548,7 +49833,6 @@ function createChatSession({
49548
49833
  deleteMessage,
49549
49834
  deleteMessages,
49550
49835
  deleteMessageDocs,
49551
- resolveActiveCall,
49552
49836
  setChatTtl,
49553
49837
  makeMessagePermanent,
49554
49838
  makeMessageTemporary,
@@ -49565,6 +49849,7 @@ function createChatSession({
49565
49849
  diag,
49566
49850
  readWriteInterval,
49567
49851
  adoptLocalMessageMedia,
49852
+ getActive: () => isActive,
49568
49853
  getIdentity: () => ({
49569
49854
  uid,
49570
49855
  chatPK,
@@ -49614,6 +49899,7 @@ function createChatSession({
49614
49899
  const setActive = (nextActive) => {
49615
49900
  foreground = nextActive === true;
49616
49901
  const value = foreground && online;
49902
+ actionOwner.sendActions.setActive(value);
49617
49903
  if (isActive === value)
49618
49904
  return;
49619
49905
  if (!value) {
@@ -49641,7 +49927,7 @@ function createChatSession({
49641
49927
  inboxTransport.setSource(inboxSource());
49642
49928
  };
49643
49929
  const setSources = (nextSources = {}) => {
49644
- const previousBlockedKey = [...blocked].sort().join("|");
49930
+ const previousBlockedKey = blockedAuthority(committedBlocked());
49645
49931
  const current = { uid, chatPK, chatBanned, chatPrivateKey, chatSigningPK, chatSigningSecret, notificationPK, notificationPrivateKey, chatAdmission, localCache };
49646
49932
  const next = normalizeChatSessionSources(nextSources);
49647
49933
  const admissionChanged = !sameChatAdmission(chatAdmission, next.chatAdmission);
@@ -49658,6 +49944,7 @@ function createChatSession({
49658
49944
  uid,
49659
49945
  online,
49660
49946
  blocked,
49947
+ blockedPending,
49661
49948
  blockedReady,
49662
49949
  chatPK,
49663
49950
  chatBanned,
@@ -49692,9 +49979,9 @@ function createChatSession({
49692
49979
  drainMembershipOutbox();
49693
49980
  replenishMlsPool({ force: true });
49694
49981
  }
49695
- const nextBlockedKey = [...blocked].sort().join("|");
49982
+ const nextBlockedKey = blockedAuthority(committedBlocked());
49696
49983
  if (online && blockedReady && nextBlockedKey && nextBlockedKey !== previousBlockedKey) {
49697
- retireBlockedChats(new Set(blocked)).catch((error) => {
49984
+ retireBlockedChats(new Set(committedBlocked())).catch((error) => {
49698
49985
  markDiag(diag, "chat.block.retire.failed", { message: error?.message || String(error) });
49699
49986
  });
49700
49987
  }
@@ -49714,8 +50001,8 @@ function createChatSession({
49714
50001
  publish();
49715
50002
  inboxTransport.start(inboxSource());
49716
50003
  chatListOwner.start();
49717
- if (blockedReady && blocked.length) {
49718
- retireBlockedChats(new Set(blocked)).catch((error) => {
50004
+ if (blockedReady && committedBlocked().length) {
50005
+ retireBlockedChats(new Set(committedBlocked())).catch((error) => {
49719
50006
  markDiag(diag, "chat.block.retire.failed", { message: error?.message || String(error) });
49720
50007
  });
49721
50008
  }
@@ -49728,6 +50015,7 @@ function createChatSession({
49728
50015
  };
49729
50016
  const close = () => {
49730
50017
  authGeneration += 1;
50018
+ messageListeners.clear();
49731
50019
  sendReadiness.reset();
49732
50020
  pendingOwner.reset();
49733
50021
  maintenance.close();
@@ -49753,7 +50041,20 @@ function createChatSession({
49753
50041
  const subscribeFields = stateOwner.subscribeFields;
49754
50042
  const getSnapshot = stateOwner.getSnapshot;
49755
50043
  publish();
49756
- return { close, getSnapshot, refreshAdmissionProjection, setInboxEnabled, setSources, start: start2, subscribe, subscribeFields };
50044
+ return {
50045
+ close,
50046
+ getSnapshot,
50047
+ refreshAdmissionProjection,
50048
+ setInboxEnabled,
50049
+ setSources,
50050
+ start: start2,
50051
+ subscribe,
50052
+ subscribeFields,
50053
+ subscribeMessages(listener) {
50054
+ messageListeners.add(listener);
50055
+ return () => messageListeners.delete(listener);
50056
+ }
50057
+ };
49757
50058
  }
49758
50059
  var DEFAULT_READ_WRITE_INTERVAL_MS, EMPTY_LOCAL_MESSAGES;
49759
50060
  var init_session3 = __esm(() => {
@@ -49961,6 +50262,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
49961
50262
  const peerListeners = new Set;
49962
50263
  const records = new Map;
49963
50264
  const peers = new Map;
50265
+ const peersByUid = new Map;
49964
50266
  const readyShards = new Map;
49965
50267
  let sources = { uid: null, identity: null, enabled: false };
49966
50268
  let channel = null;
@@ -49972,11 +50274,13 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
49972
50274
  let seq = 0;
49973
50275
  let renewTimer = null;
49974
50276
  let expiryTimer = null;
50277
+ let expiryAt = Infinity;
50278
+ let ownTopic = null;
49975
50279
  let command = null;
49976
50280
  let authenticating = null;
49977
50281
  let snapshot = Object.freeze({ visibility: null, ready: false, pending: false, pendingVisibility: null, error: null, ...UNKNOWN_PRESENCE });
49978
50282
  function topic() {
49979
- return sources.uid ? derivePresenceTopic(realm2, sources.uid) : null;
50283
+ return ownTopic;
49980
50284
  }
49981
50285
  function isReady() {
49982
50286
  return Boolean(channel && challenge && policy && sources.enabled && !closed);
@@ -50009,17 +50313,19 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50009
50313
  const latest = fresh.reduce((selected, lease) => !selected || lease.issuedAt > selected.issuedAt || lease.issuedAt === selected.issuedAt && lease.session > selected.session ? lease : selected, null);
50010
50314
  return { availability: fresh.length ? "online" : "offline", status: latest?.status ?? null, lastActiveAt };
50011
50315
  }
50012
- function refresh() {
50013
- clearTimeout(expiryTimer);
50014
- expiryTimer = null;
50015
- let changed = false;
50316
+ function refresh(keys = records.keys(), changedUids = new Set) {
50016
50317
  let deadline = Infinity;
50017
50318
  const timestamp = now();
50018
- for (const record of records.values()) {
50319
+ for (const key of keys) {
50320
+ const record = records.get(key);
50321
+ if (!record)
50322
+ continue;
50019
50323
  const next = projection(record);
50020
50324
  if (!samePresence(record.view, next)) {
50021
50325
  record.view = Object.freeze(next);
50022
- changed = true;
50326
+ const uid = peers.get(key)?.uid;
50327
+ if (uid)
50328
+ changedUids.add(uid);
50023
50329
  }
50024
50330
  if (record.ready) {
50025
50331
  for (const lease of record.leases)
@@ -50032,11 +50338,18 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50032
50338
  }
50033
50339
  const own = records.get(topic())?.view || UNKNOWN_PRESENCE;
50034
50340
  publish(policy?.visibility === "private" ? OFFLINE_PRESENCE : own);
50035
- if (changed)
50341
+ if (changedUids.size)
50036
50342
  for (const listener of peerListeners)
50037
- listener();
50038
- if (Number.isFinite(deadline) && deadline > timestamp)
50039
- expiryTimer = setTimeout(refresh, deadline - timestamp + 1);
50343
+ listener(changedUids);
50344
+ if (deadline < expiryAt && deadline > timestamp) {
50345
+ clearTimeout(expiryTimer);
50346
+ expiryAt = deadline;
50347
+ expiryTimer = setTimeout(() => {
50348
+ expiryAt = Infinity;
50349
+ expiryTimer = null;
50350
+ refresh();
50351
+ }, deadline - timestamp + 1);
50352
+ }
50040
50353
  }
50041
50354
  function finishCommand(error = null) {
50042
50355
  if (!command)
@@ -50051,7 +50364,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50051
50364
  pending.resolve(snapshot);
50052
50365
  }
50053
50366
  function expected(identity, extra = {}) {
50054
- return { realm: realm2, topic: derivePresenceTopic(realm2, identity.uid), signingPK: identity.chatSigningPK, now: now(), ...extra };
50367
+ return { realm: realm2, topic: identity.topic, signingPK: identity.chatSigningPK, now: now(), ...extra };
50055
50368
  }
50056
50369
  function receive(frame) {
50057
50370
  const identity = peers.get(frame.topic);
@@ -50076,6 +50389,15 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50076
50389
  lastActiveAt = verifyPresenceObservation(frame.lastObservation, scope).lastActiveAt;
50077
50390
  } catch {}
50078
50391
  }
50392
+ const timestamp = now();
50393
+ if (previous?.revision === currentPolicy.revision) {
50394
+ lastActiveAt = Math.max(lastActiveAt || 0, previous.lastActiveAt || 0) || null;
50395
+ if (!frame.leases?.length && previous.ready && previous.leases.some((lease) => lease.expiresAt > timestamp)) {
50396
+ lastActiveAt = Math.max(lastActiveAt || 0, timestamp);
50397
+ }
50398
+ }
50399
+ for (const lease of leases)
50400
+ lastActiveAt = Math.max(lastActiveAt || 0, Math.min(lease.issuedAt, timestamp));
50079
50401
  }
50080
50402
  records.set(frame.topic, {
50081
50403
  ready: true,
@@ -50084,7 +50406,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50084
50406
  revision: currentPolicy?.revision ?? previous?.revision ?? 0,
50085
50407
  view: previous?.view || UNKNOWN_PRESENCE
50086
50408
  });
50087
- refresh();
50409
+ refresh([frame.topic]);
50088
50410
  } catch (error) {
50089
50411
  report(error);
50090
50412
  }
@@ -50102,10 +50424,10 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50102
50424
  if (!session || !policy || policy.visibility !== "public" || command)
50103
50425
  return;
50104
50426
  const timestamp = now();
50105
- const ownTopic = topic();
50427
+ const ownTopic2 = topic();
50106
50428
  const lease = signPresenceLease(sources.identity, {
50107
50429
  realm: realm2,
50108
- topic: ownTopic,
50430
+ topic: ownTopic2,
50109
50431
  session,
50110
50432
  challenge,
50111
50433
  policyRevision: policy.revision,
@@ -50116,11 +50438,11 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50116
50438
  });
50117
50439
  const observation = signPresenceObservation(sources.identity, {
50118
50440
  realm: realm2,
50119
- topic: ownTopic,
50441
+ topic: ownTopic2,
50120
50442
  policyRevision: policy.revision,
50121
50443
  lastActiveAt: Math.floor(timestamp / PRESENCE_LAST_ACTIVE_BUCKET_MS) * PRESENCE_LAST_ACTIVE_BUCKET_MS
50122
50444
  });
50123
- channel.send(presenceShard(ownTopic), { type: "lease", lease, observation });
50445
+ channel.send(presenceShard(ownTopic2), { type: "lease", lease, observation });
50124
50446
  }
50125
50447
  function receiveOwner(frame) {
50126
50448
  if (frame.topic !== topic() || !sources.identity)
@@ -50142,7 +50464,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50142
50464
  if (policy.visibility === "private") {
50143
50465
  const previous = records.get(topic());
50144
50466
  records.set(topic(), { ready: true, leases: [], lastActiveAt: null, revision: policy.revision, view: previous?.view || UNKNOWN_PRESENCE });
50145
- refresh();
50467
+ refresh([topic()]);
50146
50468
  }
50147
50469
  if (command && policy.revision >= command.policy.revision) {
50148
50470
  finishCommand(policy.signature === command.policy.signature ? null : new Error("activity setting changed on another device; try again"));
@@ -50253,7 +50575,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50253
50575
  function setPeers(profiles2) {
50254
50576
  const next = new Map;
50255
50577
  if (sources.uid && sources.identity)
50256
- next.set(topic(), { uid: sources.uid, chatSigningPK: sources.identity.publicKey });
50578
+ next.set(topic(), { uid: sources.uid, topic: topic(), chatSigningPK: sources.identity.publicKey });
50257
50579
  for (const profile of profiles2 || []) {
50258
50580
  if (next.size >= PRESENCE_MAX_SUBSCRIPTIONS)
50259
50581
  break;
@@ -50261,27 +50583,34 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50261
50583
  continue;
50262
50584
  if (profile.uid === sources.uid)
50263
50585
  continue;
50264
- next.set(derivePresenceTopic(realm2, profile.uid), { uid: profile.uid, chatSigningPK: profile.chatSigningPK });
50586
+ const key = peersByUid.get(profile.uid)?.topic || derivePresenceTopic(realm2, profile.uid);
50587
+ next.set(key, { uid: profile.uid, topic: key, chatSigningPK: profile.chatSigningPK });
50265
50588
  }
50266
50589
  let changed = peers.size !== next.size;
50590
+ const changedUids = new Set;
50267
50591
  for (const [key, identity] of next) {
50268
50592
  if (peers.get(key)?.chatSigningPK !== identity.chatSigningPK) {
50269
50593
  changed = true;
50594
+ changedUids.add(identity.uid);
50270
50595
  records.delete(key);
50271
50596
  }
50272
50597
  }
50598
+ for (const [key, identity] of peers)
50599
+ if (!next.has(key))
50600
+ changedUids.add(identity.uid);
50273
50601
  if (!changed)
50274
50602
  return;
50275
50603
  peers.clear();
50276
- for (const [key, value] of next)
50604
+ peersByUid.clear();
50605
+ for (const [key, value] of next) {
50277
50606
  peers.set(key, value);
50607
+ peersByUid.set(value.uid, value);
50608
+ }
50278
50609
  for (const key of records.keys())
50279
50610
  if (!peers.has(key))
50280
50611
  records.delete(key);
50281
50612
  sync();
50282
- refresh();
50283
- for (const listener of peerListeners)
50284
- listener();
50613
+ refresh([], changedUids);
50285
50614
  }
50286
50615
  function update(next) {
50287
50616
  const accountChanged = sources.uid !== next.uid;
@@ -50290,11 +50619,15 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50290
50619
  stop();
50291
50620
  policy = null;
50292
50621
  records.clear();
50293
- if (accountChanged)
50622
+ if (accountChanged) {
50294
50623
  peers.clear();
50624
+ peersByUid.clear();
50625
+ }
50295
50626
  publish({ visibility: null, error: null, ...UNKNOWN_PRESENCE });
50296
50627
  }
50297
50628
  sources = next;
50629
+ if (accountChanged)
50630
+ ownTopic = next.uid ? derivePresenceTopic(realm2, next.uid) : null;
50298
50631
  setPeers([...peers.values()]);
50299
50632
  sync();
50300
50633
  }
@@ -50335,10 +50668,10 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50335
50668
  getPeer(profile) {
50336
50669
  if (!profile?.uid)
50337
50670
  return UNKNOWN_PRESENCE;
50338
- const key = derivePresenceTopic(realm2, profile.uid);
50339
- if (peers.get(key)?.chatSigningPK !== profile.chatSigningPK)
50671
+ const identity = peersByUid.get(profile.uid);
50672
+ if (!identity || identity.chatSigningPK !== profile.chatSigningPK)
50340
50673
  return UNKNOWN_PRESENCE;
50341
- return records.get(key)?.view || UNKNOWN_PRESENCE;
50674
+ return records.get(identity.topic)?.view || UNKNOWN_PRESENCE;
50342
50675
  },
50343
50676
  subscribe(listener) {
50344
50677
  listeners.add(listener);
@@ -50357,6 +50690,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
50357
50690
  listeners.clear();
50358
50691
  peerListeners.clear();
50359
50692
  peers.clear();
50693
+ peersByUid.clear();
50360
50694
  records.clear();
50361
50695
  }
50362
50696
  });
@@ -50627,6 +50961,10 @@ function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onEr
50627
50961
  }
50628
50962
  return Object.freeze({
50629
50963
  getSnapshot: () => candidate || record,
50964
+ async settled() {
50965
+ await tail;
50966
+ return record;
50967
+ },
50630
50968
  async ready() {
50631
50969
  const response = await channel.ready();
50632
50970
  await consume(response.record);
@@ -50979,6 +51317,23 @@ var init_accountlease = __esm(() => {
50979
51317
  init_wire();
50980
51318
  });
50981
51319
 
51320
+ // ../../core/calls/sources.js
51321
+ function callVideoCapacity(participants, holder, count = 1) {
51322
+ return participants.filter((peer) => peer.holder !== holder).reduce((count2, peer) => count2 + (peer.media?.sources?.length || 0), 0) + count <= CALL_MAX_VIDEO_SOURCES;
51323
+ }
51324
+ function assertCallVideoSources(participants, holder, media) {
51325
+ const sources = media?.sources || [];
51326
+ if (!Array.isArray(sources) || sources.length > CALL_MAX_VIDEO_SOURCES_PER_PARTICIPANT || sources.some((source) => !/^[A-Za-z0-9_-]{1,64}$/u.test(source?.id || "") || !["screen", "camera"].includes(source.kind) || !Number.isInteger(source.contextId) || source.contextId < 1 || source.contextId >= 2 ** 24) || new Set(sources.map((source) => source.id)).size !== sources.length || new Set(sources.map((source) => source.contextId)).size !== sources.length) {
51327
+ throw Object.assign(new Error("invalid call video sources"), { code: "calls/invalid-source" });
51328
+ }
51329
+ if (sources.length && !callVideoCapacity(participants, holder, sources.length)) {
51330
+ throw Object.assign(new Error(`this call already has ${CALL_MAX_VIDEO_SOURCES} cameras or screens`), { code: "calls/video-full" });
51331
+ }
51332
+ }
51333
+ var init_sources = __esm(() => {
51334
+ init_config();
51335
+ });
51336
+
50982
51337
  // ../../core/calls/room.js
50983
51338
  function openCallRoom({
50984
51339
  cloud,
@@ -51065,6 +51420,14 @@ function openCallRoom({
51065
51420
  settleAdmission(admissionError("calls/taken-over", "call moved to another device"));
51066
51421
  }
51067
51422
  }
51423
+ function retireMembership() {
51424
+ departing = true;
51425
+ joined = false;
51426
+ clearTimeout(pulseTimer);
51427
+ disposeState(state);
51428
+ state = null;
51429
+ onRemoved?.();
51430
+ }
51068
51431
  async function adopt(next, admissions) {
51069
51432
  verifyCallMembers(next, admissions, protocol);
51070
51433
  if (closed) {
@@ -51076,7 +51439,7 @@ function openCallRoom({
51076
51439
  disposeState(previous);
51077
51440
  const self2 = next.members.find((item) => toHex(item.leafId) === localHolder);
51078
51441
  if (!self2) {
51079
- onRemoved?.();
51442
+ retireMembership();
51080
51443
  return;
51081
51444
  }
51082
51445
  if (!admissions.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey))
@@ -51150,7 +51513,15 @@ function openCallRoom({
51150
51513
  if (bound)
51151
51514
  leaving.add(packet.holder);
51152
51515
  } else if (packet.kind === "pulse") {
51153
- published.set(packet.holder, packet.data);
51516
+ let media = packet.data.media;
51517
+ try {
51518
+ assertCallVideoSources(participants(), packet.holder, media);
51519
+ } catch {
51520
+ media = published.get(packet.holder)?.media || null;
51521
+ }
51522
+ published.set(packet.holder, { ...packet.data, media });
51523
+ if (packet.holder === localHolder)
51524
+ ownMedia = media;
51154
51525
  } else if (packet.kind === "signal") {
51155
51526
  if (packet.data.to === localHolder && head?.participants.some((item) => item.holder === packet.holder && item.actor === packet.actor)) {
51156
51527
  signals.push(packet);
@@ -51160,7 +51531,7 @@ function openCallRoom({
51160
51531
  membershipChanged = true;
51161
51532
  if (state && !replacement.participants.some((item) => item.holder === localHolder)) {
51162
51533
  settleAdmission(cancelled2());
51163
- onRemoved?.();
51534
+ retireMembership();
51164
51535
  return;
51165
51536
  }
51166
51537
  head = replacement;
@@ -51176,7 +51547,7 @@ function openCallRoom({
51176
51547
  pendingCandidate = null;
51177
51548
  if (next.removed) {
51178
51549
  disposeState(next);
51179
- onRemoved?.();
51550
+ retireMembership();
51180
51551
  return;
51181
51552
  }
51182
51553
  await adopt(next, change.participants);
@@ -51213,7 +51584,7 @@ function openCallRoom({
51213
51584
  verifiedHead = null;
51214
51585
  admittedIdentities.clear();
51215
51586
  if (joined) {
51216
- onRemoved?.();
51587
+ retireMembership();
51217
51588
  return;
51218
51589
  }
51219
51590
  if (submittedJoin)
@@ -51317,7 +51688,7 @@ function openCallRoom({
51317
51688
  for (let attempt = 0;!closed && performance.now() < deadline; attempt += 1) {
51318
51689
  if (kind === "join" && departing)
51319
51690
  throw cancelled2();
51320
- const current = mailbox.getSnapshot();
51691
+ const current = await mailbox.settled();
51321
51692
  if (!current?.head)
51322
51693
  throw kind === "join" ? empty() : new Error("call ended");
51323
51694
  if (kind === "join") {
@@ -51326,9 +51697,14 @@ function openCallRoom({
51326
51697
  throw admissionFailure;
51327
51698
  }
51328
51699
  try {
51700
+ const value = typeof data === "function" ? data() : data;
51701
+ if (kind === "pulse")
51702
+ assertCallVideoSources(participants(), localHolder, value.media);
51329
51703
  if (kind === "join")
51330
51704
  submittedJoin = true;
51331
- return await mailbox.append([protocol.sign(kind, data)], { expectedRevision: current.revision });
51705
+ const result = await mailbox.append([protocol.sign(kind, value)], { expectedRevision: current.revision });
51706
+ await mailbox.settled();
51707
+ return result;
51332
51708
  } catch (error) {
51333
51709
  if (closed)
51334
51710
  throw cancelled2();
@@ -51359,7 +51735,7 @@ function openCallRoom({
51359
51735
  if (closed || departing || !joined)
51360
51736
  return;
51361
51737
  try {
51362
- await event("pulse", { ...audio, media: ownMedia });
51738
+ await event("pulse", () => ({ ...audio, media: ownMedia }));
51363
51739
  } catch (error) {
51364
51740
  fail(error);
51365
51741
  }
@@ -51441,13 +51817,12 @@ function openCallRoom({
51441
51817
  return event("signal", { to, signal });
51442
51818
  },
51443
51819
  async publishMedia(media) {
51444
- ownMedia = media;
51445
- await event("pulse", { ...audio, media });
51820
+ await event("pulse", () => ({ ...audio, media }));
51446
51821
  },
51447
51822
  async setAudio(value) {
51448
51823
  audio = { muted: value.muted === true || value.deafened === true, deafened: value.deafened === true };
51449
51824
  if (joined && !departing && !closed)
51450
- await event("pulse", { ...audio, media: ownMedia });
51825
+ await event("pulse", () => ({ ...audio, media: ownMedia }));
51451
51826
  },
51452
51827
  async leave() {
51453
51828
  if (closed)
@@ -51485,6 +51860,7 @@ var init_room = __esm(() => {
51485
51860
  init_wire();
51486
51861
  init_protocol3();
51487
51862
  init_mailbox();
51863
+ init_sources();
51488
51864
  });
51489
51865
 
51490
51866
  // ../../core/calls/activity.js
@@ -51644,15 +52020,15 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
51644
52020
  const pendingSignals = [];
51645
52021
  const incomingSignals = [];
51646
52022
  function sourcesFor(peer) {
51647
- if (!port.capabilities?.video || !videoCapable(peer) || !Array.isArray(peer.media.sources) || peer.media.sources.length > 2)
52023
+ if (!port.capabilities?.video || !videoCapable(peer) || !Array.isArray(peer.media.sources) || peer.media.sources.length > CALL_MAX_VIDEO_SOURCES_PER_PARTICIPANT)
51648
52024
  return [];
51649
- const sources = peer.media.sources;
52025
+ const sources = peer.media.sources.filter((source) => mode === "group" ? source?.trackName != null : source?.mid != null);
51650
52026
  if (sources.some((source) => !validSourceId(source?.id) || !["screen", "camera"].includes(source.kind) || !validContext(source.contextId) || (mode === "group" ? !/^[a-f0-9]{32}$/u.test(source.trackName || "") : !validMid(source.mid))) || new Set(sources.map((source) => source.id)).size !== sources.length || new Set(sources.map((source) => source.contextId)).size !== sources.length || new Set(sources.map((source) => mode === "group" ? source.trackName : source.mid)).size !== sources.length)
51651
52027
  return [];
51652
52028
  return sources;
51653
52029
  }
51654
52030
  function videoProjection() {
51655
- const next = [...localSources.values()].map((source) => ({ id: source.id, holder, kind: source.kind, stream: source.stream, local: true }));
52031
+ const next = [...localSources.values()].filter((source) => source.admitted).map((source) => ({ id: source.id, holder, kind: source.kind, stream: source.stream, local: true }));
51656
52032
  for (const peer of peers) {
51657
52033
  if (peer.holder === holder)
51658
52034
  continue;
@@ -51672,11 +52048,11 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
51672
52048
  const base2 = mode === "group" ? { sessionId, trackName: microphoneTrack } : {};
51673
52049
  if (!port.capabilities?.video)
51674
52050
  return base2;
51675
- return { ...base2, capabilities: ["video"], sources: [...localSources.values()].filter((source) => source.published).map((source) => ({
52051
+ return { ...base2, capabilities: ["video"], sources: [...localSources.values()].map((source) => ({
51676
52052
  id: source.id,
51677
52053
  kind: source.kind,
51678
52054
  contextId: source.contextId,
51679
- ...mode === "group" ? { trackName: source.trackName } : { mid: source.mid }
52055
+ ...source.published ? mode === "group" ? { trackName: source.trackName } : { mid: source.mid } : {}
51680
52056
  })) };
51681
52057
  }
51682
52058
  async function publishMedia() {
@@ -52131,7 +52507,7 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
52131
52507
  current(generation);
52132
52508
  if (videoCapable(peer))
52133
52509
  for (const source of localSources.values()) {
52134
- if (source.attached)
52510
+ if (!source.admitted || source.attached)
52135
52511
  continue;
52136
52512
  media.addSource(source);
52137
52513
  source.attached = true;
@@ -52260,68 +52636,101 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
52260
52636
  current();
52261
52637
  videoProjection();
52262
52638
  }
52639
+ function detachSource(source) {
52640
+ localSources.delete(source.id);
52641
+ source.stream.getTracks().forEach((track) => track.stop());
52642
+ if (source.attached)
52643
+ media?.removeSource(source.id);
52644
+ videoProjection();
52645
+ }
52646
+ async function closeSourceTrack(source) {
52647
+ if (mode === "group" && source.published) {
52648
+ await applyDescription(await provider("tracks/close", { sessionId, tracks: [{ mid: source.mid }] }));
52649
+ }
52650
+ }
52263
52651
  return Object.freeze({
52264
- supportsScreen() {
52265
- return port.capabilities?.screen === true && port.capabilities?.video === true;
52652
+ supportsSource(kind) {
52653
+ return port.capabilities?.[kind] === true && port.capabilities?.video === true;
52266
52654
  },
52267
- async addSource(source) {
52655
+ async addSource(source, replaceId = null) {
52268
52656
  current();
52269
52657
  if (!started || !media?.addSource || !port.capabilities?.video)
52270
52658
  throw new Error("video sharing unavailable");
52271
- if (!validSourceId(source?.id) || !["screen", "camera"].includes(source.kind) || !validContext(source.contextId) || sourceContexts.has(source.contextId) || localSources.has(source.id) || localSources.size >= 2)
52659
+ const previous = localSources.get(replaceId);
52660
+ if (!validSourceId(source?.id) || !["screen", "camera"].includes(source.kind) || !validContext(source.contextId) || sourceContexts.has(source.contextId) || localSources.has(source.id) || localSources.size - (previous ? 1 : 0) >= CALL_MAX_VIDEO_SOURCES_PER_PARTICIPANT)
52272
52661
  throw new Error("invalid call source");
52273
52662
  const generation = connectionGeneration;
52274
- if (mode === "group")
52275
- media.addSource(source);
52276
- const local = { ...source, attached: mode === "group", published: false };
52663
+ if (previous)
52664
+ detachSource(previous);
52665
+ const local = { ...source, attached: false, admitted: false, published: false };
52277
52666
  localSources.set(source.id, local);
52278
52667
  sourceContexts.add(source.contextId);
52279
- videoProjection();
52280
- await updateKeys();
52281
- current(generation);
52282
- return serialize(async () => {
52283
- current(generation);
52284
- if (localSources.get(source.id) !== local)
52285
- return;
52286
- if (mode === "group") {
52287
- const offer = await media.offer();
52668
+ const result = await serialize(async () => {
52669
+ try {
52288
52670
  current(generation);
52289
- local.mid = media.sourceMid(source.id);
52290
- local.trackName = toHex(randomBytes3(16));
52291
- if (!validMid(local.mid))
52292
- throw new Error("video track unavailable");
52293
- const result = await provider("tracks/new", { sessionId, sessionDescription: sdp(offer), tracks: [{ location: "local", mid: local.mid, trackName: local.trackName }] });
52671
+ if (previous)
52672
+ await closeSourceTrack(previous);
52673
+ if (localSources.get(source.id) !== local)
52674
+ return;
52675
+ await publishMedia();
52294
52676
  current(generation);
52295
- if (result?.tracks?.length !== 1 || result.tracks[0].mid !== local.mid || result.tracks[0].trackName !== local.trackName)
52296
- throw new Error("video publication failed");
52297
- await applyDescription(result, true);
52677
+ if (localSources.get(source.id) !== local)
52678
+ return;
52679
+ local.admitted = true;
52680
+ if (mode === "group") {
52681
+ media.addSource(local);
52682
+ local.attached = true;
52683
+ }
52684
+ videoProjection();
52685
+ await updateKeys();
52298
52686
  current(generation);
52299
- local.published = true;
52687
+ if (localSources.get(source.id) !== local)
52688
+ return;
52689
+ if (mode === "group") {
52690
+ const offer = await media.offer();
52691
+ current(generation);
52692
+ local.mid = media.sourceMid(source.id);
52693
+ local.trackName = toHex(randomBytes3(16));
52694
+ if (!validMid(local.mid))
52695
+ throw new Error("video track unavailable");
52696
+ const response = await provider("tracks/new", { sessionId, sessionDescription: sdp(offer), tracks: [{ location: "local", mid: local.mid, trackName: local.trackName }] });
52697
+ current(generation);
52698
+ if (response?.tracks?.length !== 1 || response.tracks[0].mid !== local.mid || response.tracks[0].trackName !== local.trackName)
52699
+ throw new Error("video publication failed");
52700
+ await applyDescription(response, true);
52701
+ current(generation);
52702
+ local.published = true;
52703
+ await publishMedia();
52704
+ } else
52705
+ await negotiate();
52706
+ } catch (error) {
52707
+ if (closed || generation !== connectionGeneration || !["calls/video-full", "calls/invalid-source"].includes(error?.code))
52708
+ throw error;
52709
+ if (localSources.get(source.id) === local)
52710
+ detachSource(local);
52711
+ await updateKeys();
52300
52712
  await publishMedia();
52301
- } else
52302
- await negotiate();
52713
+ await closeSourceTrack(local);
52714
+ return { error };
52715
+ }
52303
52716
  });
52717
+ if (result?.error)
52718
+ throw result.error;
52304
52719
  },
52305
52720
  removeSource(id) {
52306
52721
  current();
52307
52722
  const local = localSources.get(id);
52308
52723
  if (!local)
52309
52724
  return Promise.resolve();
52310
- localSources.delete(id);
52311
- local.stream.getTracks().forEach((track) => track.stop());
52312
- if (local.attached)
52313
- media?.removeSource(id);
52314
- videoProjection();
52725
+ detachSource(local);
52315
52726
  const removedKeys = updateKeys();
52316
52727
  removedKeys.catch(() => {});
52317
52728
  return serialize(async () => {
52318
52729
  await removedKeys;
52319
52730
  current();
52320
52731
  await publishMedia();
52321
- if (mode === "group") {
52322
- if (local.published)
52323
- await applyDescription(await provider("tracks/close", { sessionId, tracks: [{ mid: local.mid }] }));
52324
- } else if (local.attached) {
52732
+ await closeSourceTrack(local);
52733
+ if (mode === "direct" && local.attached) {
52325
52734
  directDirty = true;
52326
52735
  await negotiate();
52327
52736
  }
@@ -52514,6 +52923,7 @@ var sdp = (value) => ({ type: value.type, sdp: value.sdp }), validMid = (value)
52514
52923
  var init_media5 = __esm(() => {
52515
52924
  init_core();
52516
52925
  init_activity();
52926
+ init_config();
52517
52927
  retiredConnection = new Error("call connection retired");
52518
52928
  });
52519
52929
 
@@ -52612,6 +53022,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52612
53022
  const discoveries = new Map;
52613
53023
  const discoveryReads = new Map;
52614
53024
  const observers = new Map;
53025
+ const messageHints = new Map;
52615
53026
  const retiring = new Set;
52616
53027
  let state = initial();
52617
53028
  const confirmedPeerAudio = new Map;
@@ -52619,6 +53030,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52619
53030
  let peerAudioSource = null;
52620
53031
  let audioSource = normalizeCallAudio();
52621
53032
  let pendingAudio = null;
53033
+ let audioSaveTail = Promise.resolve();
52622
53034
  let identity = null;
52623
53035
  let endpoint = null;
52624
53036
  let leaseEndpoint = null;
@@ -52655,7 +53067,27 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52655
53067
  diag?.("call.message.error", { event, code: error?.code || "" });
52656
53068
  });
52657
53069
  }
52658
- const canShareScreen = (call2) => !!call2 && !call2.leaving && call2.mediaReady && !!mediaPort?.captureScreen && ["connecting", "connected", "waiting"].includes(state.phase) && call2.media?.supportsScreen() === true;
53070
+ const captureFor = (kind) => kind === "camera" ? mediaPort?.captureCamera : mediaPort?.captureScreen;
53071
+ const canShare = (call2, kind) => !!call2 && !call2.leaving && call2.mediaReady && !!captureFor(kind) && ["connecting", "connected", "waiting"].includes(state.phase) && call2.media?.supportsSource(kind) === true && callVideoCapacity(call2.participants, call2.endpoint.holder);
53072
+ const sourceState = (call2) => ({
53073
+ sharingScreen: call2?.visual?.kind === "screen",
53074
+ sharingCamera: call2?.visual?.kind === "camera",
53075
+ screenShareAvailable: canShare(call2, "screen"),
53076
+ cameraAvailable: canShare(call2, "camera")
53077
+ });
53078
+ async function captureVisual(operation) {
53079
+ while (operation.visual) {
53080
+ const source = operation.visual;
53081
+ try {
53082
+ await source.capture;
53083
+ } catch (error) {
53084
+ if (operation.visual === source)
53085
+ throw error;
53086
+ }
53087
+ if (operation.visual === source)
53088
+ return;
53089
+ }
53090
+ }
52659
53091
  function current(token) {
52660
53092
  if (closed || !identity || token !== generation)
52661
53093
  throw cancelled2();
@@ -52676,17 +53108,29 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52676
53108
  }
52677
53109
  function available() {
52678
53110
  const entry = discoveries.get(focused);
52679
- const value = (chatId, current2) => current2?.descriptor ? {
52680
- chatId,
52681
- callId: current2.descriptor.callId,
52682
- startedAt: current2.descriptor.startedAt,
52683
- participants: current2.descriptor.participants || 1,
52684
- roster: (active?.discovery === current2 ? active.participants : current2.roster).filter((peer) => ![...retiring].some((call2) => call2.callId === current2.descriptor.callId && call2.endpoint.holder === peer.holder))
52685
- } : null;
53111
+ const value = (chatId, current2) => {
53112
+ if (!current2?.descriptor)
53113
+ return null;
53114
+ const roster = (active?.discovery === current2 ? active.participants : current2.roster)?.filter((peer) => ![...retiring].some((call2) => call2.callId === current2.descriptor.callId && call2.endpoint.holder === peer.holder));
53115
+ if (roster?.length === 0)
53116
+ return null;
53117
+ return {
53118
+ chatId,
53119
+ callId: current2.descriptor.callId,
53120
+ startedAt: current2.descriptor.startedAt,
53121
+ participants: roster?.length ?? (current2.descriptor.participants || 1),
53122
+ roster: roster || []
53123
+ };
53124
+ };
52686
53125
  for (const observer of observers.values()) {
53126
+ const observed = discoveries.get(observer.chatId);
53127
+ if (!observer.resolved && observed?.revision !== null && observed?.revision !== undefined) {
53128
+ observer.resolved = true;
53129
+ diag?.("call.discovery.observed", { elapsedMs: Date.now() - observer.startedAt, active: !!observed.descriptor });
53130
+ }
52687
53131
  if (!observer.onAvailable)
52688
53132
  continue;
52689
- const next2 = value(observer.chatId, discoveries.get(observer.chatId));
53133
+ const next2 = value(observer.chatId, observed);
52690
53134
  const fingerprint = JSON.stringify(next2);
52691
53135
  if (observer.fingerprint === fingerprint)
52692
53136
  continue;
@@ -52722,7 +53166,6 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52722
53166
  return;
52723
53167
  }
52724
53168
  entry.observation?.close();
52725
- entry.roster = [];
52726
53169
  entry.observedCallId = callId;
52727
53170
  const token = generation;
52728
53171
  const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint, callId });
@@ -52753,7 +53196,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52753
53196
  }
52754
53197
  function prune() {
52755
53198
  for (const [chatId, value] of discoveries) {
52756
- if (![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call2) => call2.chatId === chatId)) {
53199
+ if (!messageHints.get(chatId)?.watch && !discoveryReads.has(chatId) && !value.finishing.size && ![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call2) => call2.chatId === chatId)) {
52757
53200
  closeDiscovery(value);
52758
53201
  discoveries.delete(chatId);
52759
53202
  } else
@@ -52775,7 +53218,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52775
53218
  mailbox: null,
52776
53219
  observation: null,
52777
53220
  observedCallId: null,
52778
- roster: [],
53221
+ roster: null,
52779
53222
  revision: null,
52780
53223
  expiredCallId: null,
52781
53224
  finishing: new Map
@@ -52792,17 +53235,25 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52792
53235
  if (descriptor && (!/^[a-f0-9]{64}$/u.test(descriptor.callId) || !/^[a-f0-9]{64}$/u.test(descriptor.secret) || descriptor.mode !== epochState.manifest.lineage || !Number.isSafeInteger(descriptor.startedAt) || descriptor.startedAt < 0 || descriptor.startedAt > Date.now() + 5000))
52793
53236
  throw new Error("invalid call discovery");
52794
53237
  const previous = entry.descriptor;
53238
+ if (previous?.callId !== descriptor?.callId)
53239
+ entry.roster = null;
52795
53240
  entry.expiredCallId = !record.head ? previous?.callId || entry.expiredCallId : null;
52796
53241
  entry.descriptor = descriptor;
52797
53242
  if (!record.head && previous)
52798
53243
  finishDiscovery(entry, previous);
52799
53244
  const changed = entry.revision !== record.revision;
52800
53245
  entry.revision = record.revision;
53246
+ if (!descriptor) {
53247
+ const hint = messageHints.get(chatId);
53248
+ if (hint)
53249
+ hint.watch = false;
53250
+ }
52801
53251
  if (active?.discovery === entry && active.published && descriptor?.callId !== active.callId) {
52802
53252
  leave();
52803
53253
  }
52804
53254
  observeRoster(entry, changed);
52805
53255
  available();
53256
+ prune();
52806
53257
  },
52807
53258
  onError(error) {
52808
53259
  if (token !== generation || discoveries.get(chatId) !== entry)
@@ -52849,7 +53300,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52849
53300
  function observe(chatId, onAvailable) {
52850
53301
  if (!chatId || closed)
52851
53302
  return () => {};
52852
- const observer = { chatId, onAvailable, fingerprint: undefined };
53303
+ const observer = { chatId, onAvailable, fingerprint: undefined, startedAt: Date.now(), resolved: false };
52853
53304
  observers.set(observer, observer);
52854
53305
  focused = chatId;
52855
53306
  if (state.error?.chatId === chatId)
@@ -52901,11 +53352,11 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52901
53352
  })();
52902
53353
  const destination = (async () => {
52903
53354
  if (!onlyExisting && !continuation)
52904
- chatId = await chat.getSnapshot().materializeChat(chatId);
53355
+ chatId = await chat.getSnapshot().materializeChat(chatId, { signal: operation.controller.signal });
52905
53356
  check();
52906
53357
  joining.chatId = chatId;
52907
53358
  step("chat");
52908
- const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId);
53359
+ const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId, { signal: operation.controller.signal });
52909
53360
  check();
52910
53361
  if (!epochState)
52911
53362
  throw cancelled2();
@@ -52914,7 +53365,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52914
53365
  step("discovery");
52915
53366
  return entry2;
52916
53367
  })();
52917
- const [, entry] = await Promise.all([permission, destination, operation.screen?.capture]);
53368
+ const [, entry] = await Promise.all([permission, destination, captureVisual(operation)]);
52918
53369
  check();
52919
53370
  const discoveryRecord = entry.mailbox.getSnapshot();
52920
53371
  const create = !entry.descriptor;
@@ -52950,7 +53401,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52950
53401
  wasReady: false,
52951
53402
  signalingReady: true,
52952
53403
  ownershipReady: true,
52953
- screen: null,
53404
+ visual: null,
53405
+ capturing: null,
52954
53406
  sourceContext: 0,
52955
53407
  leaving: false,
52956
53408
  muteTail: Promise.resolve()
@@ -52996,7 +53448,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
52996
53448
  const waiting = call2.mediaReady && descriptor.mode === "direct" && state.participants.length < 2;
52997
53449
  const ready = call2.mediaState === "connected" || waiting;
52998
53450
  if (ready && call2.signalingReady && call2.ownershipReady) {
52999
- call2.wasReady = true;
53451
+ call2.wasReady = !waiting;
53000
53452
  clearTimeout(call2.joinTimer);
53001
53453
  call2.joinTimer = null;
53002
53454
  emit({ phase: waiting ? "waiting" : "connected" });
@@ -53010,7 +53462,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53010
53462
  fail(new Error("call did not connect"));
53011
53463
  }, 30000);
53012
53464
  }
53013
- emit({ screenShareAvailable: canShareScreen(call2) });
53465
+ emit(sourceState(call2));
53014
53466
  };
53015
53467
  call2.connectionProgress = connectionProgress;
53016
53468
  call2.media = createCallMediaSession({
@@ -53057,8 +53509,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53057
53509
  emit({ videoSources });
53058
53510
  },
53059
53511
  onSourceEnded(id) {
53060
- if (valid() && call2.screen?.id === id)
53061
- stopScreenShare().catch(() => {});
53512
+ if (valid() && call2.visual?.id === id)
53513
+ stopVisual().catch(() => {});
53062
53514
  },
53063
53515
  onError(error) {
53064
53516
  if (valid())
@@ -53103,7 +53555,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53103
53555
  if (preference)
53104
53556
  call2.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
53105
53557
  }
53106
- emit({ participants: value, screenShareAvailable: canShareScreen(call2) });
53558
+ emit({ participants: value, ...sourceState(call2) });
53107
53559
  available();
53108
53560
  call2.refresh?.();
53109
53561
  if (call2.mediaReady)
@@ -53134,6 +53586,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53134
53586
  checkCall();
53135
53587
  await call2.room.start({ create });
53136
53588
  checkCall();
53589
+ if (operation.visual)
53590
+ assertCallVideoSources(call2.participants, callEndpoint.holder, { sources: [{ ...operation.visual, contextId: 1 }] });
53137
53591
  step("admission");
53138
53592
  if (create) {
53139
53593
  try {
@@ -53235,12 +53689,14 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53235
53689
  };
53236
53690
  call2.refresh = refresh;
53237
53691
  refresh(true);
53238
- if (operation.screen) {
53239
- await operation.screen.capture;
53692
+ while (operation.visual) {
53693
+ await captureVisual(operation);
53240
53694
  check();
53241
- await publishScreen(call2, operation.screen);
53695
+ const source = operation.visual;
53696
+ await publishVisual(call2, source);
53242
53697
  check();
53243
- operation.screen = null;
53698
+ if (operation.visual === source)
53699
+ operation.visual = null;
53244
53700
  }
53245
53701
  return chatId;
53246
53702
  } catch (error) {
@@ -53273,20 +53729,21 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53273
53729
  throw new Error("open veyl to join a call");
53274
53730
  return runJoin(chatId, expectedCallId, onlyExisting);
53275
53731
  }
53276
- function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null, screen = null) {
53732
+ function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null, visual = null) {
53277
53733
  const intent = ++joinIntent;
53278
- const operation = { predecessor: active, chatId, candidate: null, continuation, screen };
53734
+ const operation = { predecessor: active, chatId, candidate: null, continuation, visual, controller: new AbortController };
53279
53735
  let resolve, reject;
53280
53736
  operation.promise = new Promise((done, fail2) => {
53281
53737
  resolve = done;
53282
53738
  reject = fail2;
53283
53739
  });
53284
- if (screen)
53285
- screen.promise = operation.promise;
53740
+ if (visual)
53741
+ visual.promise = operation.promise;
53286
53742
  const previous = joining;
53287
53743
  joining = operation;
53744
+ previous?.controller.abort();
53288
53745
  previous?.preparation?.close();
53289
- previous?.screen?.stop();
53746
+ previous?.visual?.stop();
53290
53747
  emit({ joiningChatId: chatId });
53291
53748
  if (previous?.candidate && previous.candidate !== active)
53292
53749
  disposeCall(previous.candidate);
@@ -53310,7 +53767,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53310
53767
  }
53311
53768
  }
53312
53769
  } finally {
53313
- operation.screen?.stop();
53770
+ operation.controller.abort();
53771
+ operation.visual?.stop();
53314
53772
  if (joining === operation) {
53315
53773
  joining = null;
53316
53774
  emit({ joiningChatId: null });
@@ -53322,8 +53780,9 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53322
53780
  }
53323
53781
  function leave() {
53324
53782
  joinIntent += 1;
53783
+ joining?.controller.abort();
53325
53784
  joining?.preparation?.close();
53326
- joining?.screen?.stop();
53785
+ joining?.visual?.stop();
53327
53786
  const candidate = joining?.candidate;
53328
53787
  joining = null;
53329
53788
  emit({ joiningChatId: null });
@@ -53338,8 +53797,9 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53338
53797
  if (!operation)
53339
53798
  return Promise.resolve();
53340
53799
  joinIntent += 1;
53800
+ operation.controller.abort();
53341
53801
  operation.preparation?.close();
53342
- operation.screen?.stop();
53802
+ operation.visual?.stop();
53343
53803
  joining = null;
53344
53804
  emit({ joiningChatId: null });
53345
53805
  const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
@@ -53358,15 +53818,17 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53358
53818
  leaving = retired.length ? Promise.race([released, Promise.all(retired.map((room) => room.closing))]) : released;
53359
53819
  }
53360
53820
  if (!call2) {
53361
- emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false });
53821
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false, sharingCamera: false, cameraAvailable: false });
53362
53822
  return leaving;
53363
53823
  }
53824
+ if (call2.discovery.descriptor?.callId === call2.callId)
53825
+ call2.discovery.roster = call2.participants;
53364
53826
  active = null;
53365
53827
  leaving = disposeCall(call2);
53366
53828
  if (identity && discoveries.get(call2.chatId) === call2.discovery)
53367
53829
  observeRoster(call2.discovery);
53368
53830
  available();
53369
- emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false });
53831
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false, sharingCamera: false, cameraAvailable: false });
53370
53832
  return leaving;
53371
53833
  }
53372
53834
  async function retireDiscovery(call2) {
@@ -53387,15 +53849,20 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53387
53849
  function finishDiscovery(entry, descriptor) {
53388
53850
  if (entry.finishing.has(descriptor.callId))
53389
53851
  return;
53390
- const finishing = retireDiscovery({ discovery: entry, callId: descriptor.callId, chatId: entry.epochState.manifest.chatId }).catch((error) => diag?.("call.close.error", { code: error?.code || "calls/discovery" })).finally(() => entry.finishing.delete(descriptor.callId));
53852
+ const finishing = retireDiscovery({ discovery: entry, callId: descriptor.callId, chatId: entry.epochState.manifest.chatId }).catch((error) => diag?.("call.close.error", { code: error?.code || "calls/discovery" })).finally(() => {
53853
+ entry.finishing.delete(descriptor.callId);
53854
+ prune();
53855
+ });
53391
53856
  entry.finishing.set(descriptor.callId, finishing);
53392
53857
  }
53393
53858
  function disposeCall(call2) {
53394
53859
  if (call2.closing)
53395
53860
  return call2.closing;
53396
53861
  call2.leaving = true;
53397
- call2.screen?.stop();
53398
- call2.screen = null;
53862
+ call2.capturing?.stop();
53863
+ call2.capturing = null;
53864
+ call2.visual?.stop();
53865
+ call2.visual = null;
53399
53866
  retiring.add(call2);
53400
53867
  clearTimeout(call2.timer);
53401
53868
  clearTimeout(call2.joinTimer);
@@ -53437,28 +53904,40 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53437
53904
  });
53438
53905
  return call2.closing;
53439
53906
  }
53440
- function startScreenShare(chatId = active?.chatId) {
53441
- if (!identity || closed || !mediaPort?.captureScreen || !mediaPort.capabilities?.screen || !mediaPort.capabilities?.video || !mls || !cloud.calls || !chatId)
53442
- return Promise.reject(new Error("screen sharing is unavailable in this call"));
53907
+ function startVisual(kind, chatId = active?.chatId) {
53908
+ if (!identity || closed || !captureFor(kind) || !mediaPort.capabilities?.[kind] || !mediaPort.capabilities?.video || !mls || !cloud.calls || !chatId)
53909
+ return Promise.reject(new Error(`${kind === "camera" ? "camera" : "screen sharing"} is unavailable in this call`));
53443
53910
  const operation = joining && [joining.chatId, state.joiningChatId].includes(chatId) ? joining : null;
53444
- if (operation?.screen)
53445
- return operation.screen.promise;
53446
53911
  const call2 = active?.chatId === chatId ? active : null;
53447
- if (call2 && !operation && !canShareScreen(call2))
53448
- return Promise.reject(new Error("screen sharing is unavailable in this call"));
53912
+ const pending = operation?.visual || call2?.capturing;
53913
+ if (pending?.kind === kind)
53914
+ return pending.promise;
53915
+ if (!pending && call2?.visual?.kind === kind)
53916
+ return call2.visual.promise;
53917
+ if (call2 && !operation && !canShare(call2, kind)) {
53918
+ try {
53919
+ assertCallVideoSources(call2.participants, call2.endpoint.holder, { sources: [{ id: "capture", kind, contextId: 1 }] });
53920
+ } catch (error) {
53921
+ return Promise.reject(error);
53922
+ }
53923
+ return Promise.reject(new Error(`${kind} is unavailable in this call`));
53924
+ }
53449
53925
  if (!call2 && !foreground)
53450
53926
  return Promise.reject(new Error("open veyl to join a call"));
53451
- if (call2?.screen)
53452
- return call2.screen.promise;
53927
+ let rejectCapture;
53928
+ const cancellation = new Promise((_, reject) => {
53929
+ rejectCapture = reject;
53930
+ });
53453
53931
  const source = {
53454
53932
  id: toHex(randomBytes3(16)),
53455
53933
  contextId: null,
53456
- kind: "screen",
53934
+ kind,
53457
53935
  stream: null,
53458
53936
  promise: null,
53459
53937
  cancelled: false,
53460
53938
  stop() {
53461
53939
  source.cancelled = true;
53940
+ rejectCapture(cancelled2());
53462
53941
  for (const track of source.stream?.getTracks() || []) {
53463
53942
  track.removeEventListener?.("ended", sourceEnded);
53464
53943
  track.stop();
@@ -53466,18 +53945,18 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53466
53945
  }
53467
53946
  };
53468
53947
  const sourceEnded = () => {
53469
- if (joining?.screen === source)
53948
+ if (joining?.visual === source)
53470
53949
  cancelJoin();
53471
- else if (active?.screen === source)
53472
- stopScreenShare().catch(() => {});
53950
+ else if (active?.visual === source || active?.capturing === source)
53951
+ stopVisual().catch(() => {});
53473
53952
  };
53474
53953
  let capturing;
53475
53954
  try {
53476
- capturing = mediaPort.captureScreen();
53955
+ capturing = captureFor(kind).call(mediaPort);
53477
53956
  } catch (error) {
53478
53957
  return Promise.reject(error);
53479
53958
  }
53480
- source.capture = Promise.resolve(capturing).then((stream) => {
53959
+ source.capture = Promise.race([cancellation, Promise.resolve(capturing).then((stream) => {
53481
53960
  if (source.cancelled || stream.getTracks().some((track) => track.readyState === "ended")) {
53482
53961
  stream.getTracks().forEach((track) => track.stop());
53483
53962
  throw cancelled2();
@@ -53485,64 +53964,78 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53485
53964
  source.stream = stream;
53486
53965
  for (const track of stream.getTracks())
53487
53966
  track.addEventListener?.("ended", sourceEnded, { once: true });
53488
- });
53967
+ })]);
53489
53968
  if (operation) {
53969
+ operation.visual = source;
53970
+ if (pending !== call2?.visual)
53971
+ pending?.stop();
53490
53972
  source.promise = Promise.all([source.capture, operation.promise]).then(() => {
53491
53973
  return;
53492
53974
  });
53493
- operation.screen = source;
53494
53975
  return source.promise;
53495
53976
  }
53496
53977
  if (!call2) {
53497
53978
  source.promise = runJoin(chatId, null, false, null, source);
53498
53979
  return source.promise;
53499
53980
  }
53500
- call2.screen = source;
53981
+ call2.capturing = source;
53982
+ if (pending !== call2.visual)
53983
+ pending?.stop();
53501
53984
  source.promise = source.capture.then(() => {
53502
- if (!canShareScreen(call2))
53985
+ if (source.cancelled || active !== call2 || call2.capturing !== source || call2.leaving || !call2.mediaReady)
53503
53986
  throw cancelled2();
53504
- return publishScreen(call2, source);
53987
+ return publishVisual(call2, source);
53505
53988
  }).catch(async (error) => {
53506
53989
  source.stop();
53507
- if (call2.screen === source) {
53508
- call2.screen = null;
53990
+ if (call2.visual === source) {
53991
+ call2.visual = null;
53509
53992
  if (active === call2)
53510
- emit({ sharingScreen: false });
53993
+ emit(sourceState(call2));
53511
53994
  if (!call2.leaving && source.stream)
53512
53995
  await call2.media.removeSource(source.id).catch(() => {});
53513
53996
  }
53514
53997
  throw error;
53998
+ }).finally(() => {
53999
+ if (call2.capturing === source)
54000
+ call2.capturing = null;
53515
54001
  });
53516
54002
  return source.promise;
53517
54003
  }
53518
- async function publishScreen(call2, source) {
53519
- if (source.cancelled || active !== call2 || call2.leaving || !call2.mediaReady || !call2.media.supportsScreen())
54004
+ async function publishVisual(call2, source) {
54005
+ if (source.cancelled || active !== call2 || call2.leaving || !call2.mediaReady || !call2.media.supportsSource(source.kind))
53520
54006
  throw cancelled2();
54007
+ const previous = call2.visual;
53521
54008
  source.contextId = ++call2.sourceContext;
53522
- call2.screen = source;
53523
- emit({ sharingScreen: true });
53524
- await call2.media.addSource(source);
53525
- if (active !== call2 || call2.screen !== source)
54009
+ previous?.stop();
54010
+ call2.visual = source;
54011
+ emit(sourceState(call2));
54012
+ await call2.media.addSource(source, previous?.id);
54013
+ if (active !== call2 || call2.visual !== source)
53526
54014
  throw cancelled2();
53527
54015
  }
53528
- async function stopScreenShare() {
53529
- if (joining?.screen)
54016
+ async function stopVisual(kind) {
54017
+ if (joining?.visual && (!kind || joining.visual.kind === kind))
53530
54018
  return cancelJoin();
53531
54019
  const call2 = active;
53532
- const source = call2?.screen;
53533
- if (!source)
54020
+ if (call2?.capturing && (!kind || call2.capturing.kind === kind || call2.visual?.kind === kind)) {
54021
+ call2.capturing.stop();
54022
+ call2.capturing = null;
54023
+ }
54024
+ const source = call2?.visual;
54025
+ if (!source || kind && source.kind !== kind)
53534
54026
  return;
53535
- call2.screen = null;
54027
+ call2.visual = null;
53536
54028
  source.stop();
53537
- emit({ sharingScreen: false });
54029
+ emit(sourceState(call2));
53538
54030
  if (source.stream)
53539
54031
  await call2.media.removeSource(source.id);
53540
54032
  }
53541
54033
  function applyAudio(call2) {
53542
54034
  const audio = { muted: state.muted || state.deafened, deafened: state.deafened };
53543
54035
  const applied = Promise.all([call2.media.mute(audio.muted), call2.media.deafen(audio.deafened)]);
54036
+ call2.pendingAudio = audio;
53544
54037
  const result = Promise.all([applied, call2.muteTail]).then(() => {
53545
- if (!call2.leaving)
54038
+ if (!call2.leaving && call2.pendingAudio === audio)
53546
54039
  return call2.room.setAudio(audio);
53547
54040
  });
53548
54041
  call2.muteTail = result.catch(() => {});
@@ -53554,14 +54047,16 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53554
54047
  return;
53555
54048
  const token = generation;
53556
54049
  pendingAudio = value;
53557
- const saving = Promise.resolve().then(() => {
54050
+ const saving = audioSaveTail.then(async () => {
53558
54051
  if (generation !== token)
53559
54052
  throw cancelled2();
53560
- return saveAudio(value);
53561
- }).then(() => {
54053
+ if (pendingAudio !== value)
54054
+ return;
54055
+ await saveAudio(value);
53562
54056
  if (generation === token)
53563
54057
  audioSource = value;
53564
54058
  });
54059
+ audioSaveTail = saving.catch(() => {});
53565
54060
  try {
53566
54061
  await Promise.all([saving, applyAudioPreferences(value)]);
53567
54062
  } finally {
@@ -53732,6 +54227,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53732
54227
  endpoint = null;
53733
54228
  discoveries.clear();
53734
54229
  discoveryReads.clear();
54230
+ messageHints.clear();
53735
54231
  identity = session;
53736
54232
  const stopped = leave();
53737
54233
  Promise.resolve(stopped).finally(() => {
@@ -53746,6 +54242,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53746
54242
  confirmedPeerAudio.clear();
53747
54243
  pendingPeerAudio.clear();
53748
54244
  pendingAudio = null;
54245
+ audioSaveTail = Promise.resolve();
53749
54246
  audioSource = normalizeCallAudio(settings.callAudio);
53750
54247
  peerAudioSource = session ? peerAudio : null;
53751
54248
  for (const [chatPK, value] of Object.entries(peerAudioSource || {}))
@@ -53763,10 +54260,41 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53763
54260
  return retirement;
53764
54261
  }
53765
54262
  function refreshObserved() {
53766
- for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
54263
+ for (const chatId of observedChatIds()) {
53767
54264
  readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
53768
54265
  }
53769
54266
  }
54267
+ function observedChatIds() {
54268
+ return new Set([...observers.values()].map((item) => item.chatId).concat([...messageHints].filter(([, hint]) => hint.watch).map(([chatId]) => chatId)));
54269
+ }
54270
+ const unsubscribeMessages = chat.subscribeMessages((chatId, messages) => {
54271
+ if (!identity || closed)
54272
+ return;
54273
+ let latest = null;
54274
+ for (const message of messages) {
54275
+ if (!isServerConfirmedMsg(message) || !readCallMessage(message))
54276
+ continue;
54277
+ if (!latest || getMessageOrderMs(message) > getMessageOrderMs(latest) || getMessageOrderMs(message) === getMessageOrderMs(latest) && getMessageKey(message) > getMessageKey(latest))
54278
+ latest = message;
54279
+ }
54280
+ if (!latest)
54281
+ return;
54282
+ const at = getMessageOrderMs(latest), key = getMessageKey(latest);
54283
+ if (!Number.isFinite(at) || !key)
54284
+ return;
54285
+ const previous = messageHints.get(chatId);
54286
+ if (previous && (previous.at > at || previous.at === at && previous.key >= key))
54287
+ return;
54288
+ const entry = discoveries.get(chatId);
54289
+ const watch = latest.event === "started" || previous?.watch === true || !!entry?.descriptor;
54290
+ messageHints.delete(chatId);
54291
+ messageHints.set(chatId, { at, key, watch });
54292
+ while (messageHints.size > RECENT_CALL_HINT_LIMIT)
54293
+ messageHints.delete(messageHints.keys().next().value);
54294
+ prune();
54295
+ if (watch && online)
54296
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "message", code: error?.code || "calls/discovery" }));
54297
+ });
53770
54298
  const unsubscribe = chat.subscribe(() => {
53771
54299
  if (!identity)
53772
54300
  return;
@@ -53780,6 +54308,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53780
54308
  const intent = ++joinIntent;
53781
54309
  const previous = active || joining?.candidate;
53782
54310
  active = null;
54311
+ joining?.controller.abort();
53783
54312
  joining = null;
53784
54313
  if (previous)
53785
54314
  disposeCall(previous);
@@ -53791,6 +54320,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53791
54320
  videoSources: [],
53792
54321
  sharingScreen: false,
53793
54322
  screenShareAvailable: false,
54323
+ sharingCamera: false,
54324
+ cameraAvailable: false,
53794
54325
  participants: state.participants.filter((member) => members.has(member.chatPK))
53795
54326
  });
53796
54327
  diag?.("call.epoch.transition", { stage: "starting", participants: state.participants.length });
@@ -53809,11 +54340,11 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53809
54340
  closeDiscovery(entry);
53810
54341
  discoveries.delete(chatId);
53811
54342
  available();
53812
- if (online && [...observers.values()].some((item) => item.chatId === chatId))
54343
+ if (online && observedChatIds().has(chatId))
53813
54344
  readDiscovery(chatId).catch(() => {});
53814
54345
  }
53815
54346
  if (online)
53816
- for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
54347
+ for (const chatId of observedChatIds()) {
53817
54348
  if (!discoveries.has(chatId) && !discoveryReads.has(chatId) && chat.getSnapshot().getOwnerChat(chatId)?.epochState) {
53818
54349
  readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
53819
54350
  }
@@ -53831,8 +54362,10 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53831
54362
  leave,
53832
54363
  cancelJoin,
53833
54364
  setMicrophoneVolume,
53834
- startScreenShare,
53835
- stopScreenShare,
54365
+ startScreenShare: (chatId) => startVisual("screen", chatId),
54366
+ stopScreenShare: () => stopVisual("screen"),
54367
+ startCamera: (chatId) => startVisual("camera", chatId),
54368
+ stopCamera: () => stopVisual("camera"),
53836
54369
  async resolveActiveCall(chatId) {
53837
54370
  const entry = await readDiscovery(chatId);
53838
54371
  return entry.descriptor?.callId || null;
@@ -53847,6 +54380,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53847
54380
  return Promise.reject(ended());
53848
54381
  return join(chatId, callId, true);
53849
54382
  },
54383
+ setAudio,
53850
54384
  setMuted(value) {
53851
54385
  return setAudio({ muted: value === true });
53852
54386
  },
@@ -53880,6 +54414,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53880
54414
  });
53881
54415
  closed = true;
53882
54416
  const stopped = setSources(null);
54417
+ unsubscribeMessages();
53883
54418
  unsubscribe();
53884
54419
  listeners.clear();
53885
54420
  Promise.resolve(stopped).finally(() => Promise.all([mls?.close?.(), mediaPort?.close?.()])).then(finish, reject);
@@ -53887,7 +54422,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
53887
54422
  }
53888
54423
  });
53889
54424
  }
53890
- var initial = () => ({ phase: "idle", chatId: null, callId: null, joiningChatId: null, mode: null, transport: null, available: null, elsewhere: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false, peerAudio: {}, muted: false, deafened: false, microphoneVolume: 100, error: null }), cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" }), ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" }), callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
54425
+ var initial = () => ({ phase: "idle", chatId: null, callId: null, joiningChatId: null, mode: null, transport: null, available: null, elsewhere: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false, sharingCamera: false, cameraAvailable: false, peerAudio: {}, muted: false, deafened: false, microphoneVolume: 100, error: null }), cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" }), ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" }), RECENT_CALL_HINT_LIMIT = 32, callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
53891
54426
  var init_session5 = __esm(() => {
53892
54427
  init_core();
53893
54428
  init_identity();
@@ -53898,9 +54433,11 @@ var init_session5 = __esm(() => {
53898
54433
  init_media5();
53899
54434
  init_observation();
53900
54435
  init_wire();
53901
- init_call();
54436
+ init_state();
54437
+ init_control();
53902
54438
  init_ownership();
53903
54439
  init_settings();
54440
+ init_sources();
53904
54441
  });
53905
54442
 
53906
54443
  // ../../node_modules/.bun/@zxcvbn-ts+core@4.2.0/node_modules/@zxcvbn-ts/core/dist/utils/helper.cjs
@@ -57524,6 +58061,18 @@ function stablePeerMap(previous, next) {
57524
58061
  }
57525
58062
  return previous;
57526
58063
  }
58064
+ function replaceIndexedPeers(index2, updates, field) {
58065
+ let next = index2;
58066
+ for (const peer of updates.values()) {
58067
+ const key = peer[field];
58068
+ if (!key || index2.get(key)?.uid !== peer.uid)
58069
+ continue;
58070
+ if (next === index2)
58071
+ next = new Map(index2);
58072
+ next.set(key, peer);
58073
+ }
58074
+ return next;
58075
+ }
57527
58076
  function stableValueSet(previous, next) {
57528
58077
  if (previous === next)
57529
58078
  return previous;
@@ -57551,12 +58100,11 @@ function peerProfileSnapshot(peer) {
57551
58100
  active: !!peer.active,
57552
58101
  presence: peer.presence,
57553
58102
  accountType: peer.accountType || null,
57554
- blocked: !!peer.blocked,
57555
- hidden: !!peer.hidden
58103
+ blocked: !!peer.blocked
57556
58104
  };
57557
58105
  }
57558
58106
  function samePeerProfile(left, right) {
57559
- return left === right || !!left && !!right && left.uid === right.uid && left.username === right.username && left.chatPK === right.chatPK && left.chatSigningPK === right.chatSigningPK && left.notificationPK === right.notificationPK && sameChatAdmission(left.chatAdmission, right.chatAdmission) && left.walletPK === right.walletPK && left.avatar === right.avatar && left.avatarVersion === right.avatarVersion && left.active === right.active && left.presence === right.presence && left.accountType === right.accountType && left.blocked === right.blocked && left.hidden === right.hidden;
58107
+ return left === right || !!left && !!right && left.uid === right.uid && left.username === right.username && left.chatPK === right.chatPK && left.chatSigningPK === right.chatSigningPK && left.notificationPK === right.notificationPK && sameChatAdmission(left.chatAdmission, right.chatAdmission) && left.walletPK === right.walletPK && left.avatar === right.avatar && left.avatarVersion === right.avatarVersion && left.active === right.active && left.presence === right.presence && left.accountType === right.accountType && left.blocked === right.blocked;
57560
58108
  }
57561
58109
  function normalizeProfileIdentity(identity = {}) {
57562
58110
  const uid = String(identity?.uid || "").trim();
@@ -57646,7 +58194,6 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57646
58194
  let blockedUids = [];
57647
58195
  let blockedUidsKey = "";
57648
58196
  let blockedUidSet = new Set;
57649
- let hiddenPeerUidSet = new Set;
57650
58197
  let seenWalletPKs = new Set;
57651
58198
  let seenChatPKs = new Set;
57652
58199
  let walletPKs = [];
@@ -57680,7 +58227,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57680
58227
  subscriberCount += 1;
57681
58228
  if (!firstSubscriber)
57682
58229
  return;
57683
- unsubscribePresence = presence.subscribePeers(() => rebuild({ projection: true, persist: false }));
58230
+ unsubscribePresence = presence.subscribePeers(updatePresence);
57684
58231
  hydrateLocalAvatars();
57685
58232
  const becameReady = startProfileLoad();
57686
58233
  if (becameReady) {
@@ -57712,7 +58259,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57712
58259
  function resolveProfile(identity) {
57713
58260
  if (!identity)
57714
58261
  return null;
57715
- const peer = (identity.uid ? peerByUid.get(identity.uid) : null) ?? (identity.username ? peerByUsername.get(identity.username) : null) ?? (identity.chatPK ? peerByChatPK.get(identity.chatPK) : null) ?? (identity.walletPK ? peerByWalletPK.get(identity.walletPK) : null) ?? null;
58262
+ const peer = (identity.uid ? allPeerByUid.get(identity.uid) : null) ?? (identity.username ? peerByUsername.get(identity.username) : null) ?? (identity.chatPK ? peerByChatPK.get(identity.chatPK) : null) ?? (identity.walletPK ? peerByWalletPK.get(identity.walletPK) : null) ?? null;
57716
58263
  return peer?.uid ? peerProfilesByUid.get(peer.uid) || null : null;
57717
58264
  }
57718
58265
  function emitProfiles() {
@@ -57743,8 +58290,6 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57743
58290
  primePeer,
57744
58291
  findPeer,
57745
58292
  updatePeer,
57746
- dropPeer,
57747
- restorePeer,
57748
58293
  loadBlockedPeers
57749
58294
  };
57750
58295
  }
@@ -57822,7 +58367,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57822
58367
  function syncPresencePeers() {
57823
58368
  const candidates = new Map;
57824
58369
  const add2 = (profile) => {
57825
- if (!profile?.uid || profile.blocked || profile.hidden || candidates.size >= RECENT_PEER_REFRESH_LIMIT)
58370
+ if (!profile?.uid || profile.blocked || candidates.size >= RECENT_PEER_REFRESH_LIMIT)
57826
58371
  return;
57827
58372
  candidates.set(profile.uid, profile);
57828
58373
  };
@@ -57846,33 +58391,61 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57846
58391
  function buildBasePeers() {
57847
58392
  allPeers = assemblePeers(walletPeerSet, sources.chatPeers, discoveredPeerUids);
57848
58393
  }
57849
- function buildPeerProjection() {
57850
- const blockedPeerUidSet = new Set(blockedUids);
57851
- for (const peer of blockedPeers) {
57852
- if (peer?.uid)
57853
- blockedPeerUidSet.add(peer.uid);
58394
+ function projectPeer(peer) {
58395
+ const activity = presence.getPeer(peer);
58396
+ const next = {
58397
+ ...peer,
58398
+ active: activity.availability === "online",
58399
+ presence: activity,
58400
+ blocked: blockedUidSet.has(peer.uid)
58401
+ };
58402
+ const previous = allPeerByUid.get(peer.uid);
58403
+ return samePeerRecord(previous, next) ? previous : next;
58404
+ }
58405
+ function updatePresence(changedUids) {
58406
+ const updates = new Map;
58407
+ for (const uid of changedUids) {
58408
+ const previous = allPeerByUid.get(uid);
58409
+ if (!previous)
58410
+ continue;
58411
+ const next = projectPeer(previous);
58412
+ if (next !== previous)
58413
+ updates.set(uid, next);
57854
58414
  }
58415
+ if (!updates.size)
58416
+ return;
58417
+ allPeerByUid = replaceIndexedPeers(allPeerByUid, updates, "uid");
58418
+ peerByUid = replaceIndexedPeers(peerByUid, updates, "uid");
58419
+ peerByWalletPK = replaceIndexedPeers(peerByWalletPK, updates, "walletPK");
58420
+ peerByChatPK = replaceIndexedPeers(peerByChatPK, updates, "chatPK");
58421
+ peerByUsername = replaceIndexedPeers(peerByUsername, updates, "username");
58422
+ const replaceList = (list) => stablePeerList(list, list.map((peer) => updates.get(peer.uid) || peer));
58423
+ visiblePeers = replaceList(visiblePeers);
58424
+ blockedPeerItems = replaceList(blockedPeerItems);
58425
+ const all = replaceList(recentPeers.all);
58426
+ const wallet = replaceList(recentPeers.wallet);
58427
+ const chat = replaceList(recentPeers.chat);
58428
+ if (all !== recentPeers.all || wallet !== recentPeers.wallet || chat !== recentPeers.chat)
58429
+ recentPeers = { all, wallet, chat };
58430
+ peerProfilesByUid = new Map(peerProfilesByUid);
58431
+ for (const peer of updates.values()) {
58432
+ const next = peerProfileSnapshot(peer);
58433
+ const previous = peerProfilesByUid.get(peer.uid);
58434
+ peerProfilesByUid.set(peer.uid, samePeerProfile(previous, next) ? previous : next);
58435
+ }
58436
+ publish();
58437
+ }
58438
+ function buildPeerProjection() {
57855
58439
  const peerMap = new Map;
57856
58440
  for (const peer of allPeers)
57857
58441
  mergePeerRecord(peerMap, peer);
57858
58442
  for (const peer of blockedPeers)
57859
58443
  mergePeerRecord(peerMap, peer);
57860
- const taggedPeers = Array.from(peerMap.values()).map((peer) => {
57861
- const activity = presence.getPeer(peer);
57862
- const next = {
57863
- ...peer,
57864
- active: activity.availability === "online",
57865
- presence: activity,
57866
- blocked: blockedPeerUidSet.has(peer.uid) || hiddenPeerUidSet.has(peer.uid),
57867
- hidden: hiddenPeerUidSet.has(peer.uid)
57868
- };
57869
- const previous = allPeerByUid.get(peer.uid);
57870
- return samePeerRecord(previous, next) ? previous : next;
57871
- });
57872
- visiblePeers = stablePeerList(visiblePeers, taggedPeers.filter((peer) => !peer.blocked && !peer.hidden));
58444
+ const taggedPeers = Array.from(peerMap.values()).map(projectPeer);
58445
+ visiblePeers = stablePeerList(visiblePeers, taggedPeers.filter((peer) => !peer.blocked));
57873
58446
  const blockedByUid = new Map(taggedPeers.filter((peer) => peer?.uid && peer.blocked).map((peer) => [peer.uid, peer]));
57874
58447
  const orderedBlocked = blockedUids.map((uid) => blockedByUid.get(uid)).filter((peer) => peer?.uid);
57875
- blockedPeerItems = stablePeerList(blockedPeerItems, orderedBlocked.length || blockedUids.length ? orderedBlocked : Array.from(blockedByUid.values()));
58448
+ blockedPeerItems = stablePeerList(blockedPeerItems, orderedBlocked);
57876
58449
  blockedChatPKSet = stableValueSet(blockedChatPKSet, new Set(taggedPeers.filter((peer) => peer.blocked && peer.chatPK).map((peer) => peer.chatPK)));
57877
58450
  const allIndexes = indexPeers(taggedPeers);
57878
58451
  const visibleIndexes = indexPeers(visiblePeers);
@@ -57985,7 +58558,6 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
57985
58558
  blockedPeersReady = false;
57986
58559
  discoveredPeerUids = new Set;
57987
58560
  blockedPeers = [];
57988
- hiddenPeerUidSet = new Set;
57989
58561
  seenWalletPKs = new Set;
57990
58562
  seenChatPKs = new Set;
57991
58563
  allPeers = [];
@@ -58098,6 +58670,10 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
58098
58670
  blockedUids = nextBlockedUids;
58099
58671
  blockedUidsKey = nextBlockedUidsKey;
58100
58672
  blockedUidSet = new Set(blockedUids);
58673
+ for (const uid of blockedUids) {
58674
+ if (allPeerByUid.has(uid))
58675
+ discoveredPeerUids.add(uid);
58676
+ }
58101
58677
  if (blockedPeersReady) {
58102
58678
  blockedPeers = blockedPeers.filter((peer) => blockedUidSet.has(peer?.uid));
58103
58679
  }
@@ -58139,7 +58715,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
58139
58715
  const enrichedPeer = await fetchAndCachePeer(partialProfile);
58140
58716
  if (!isCurrent(session) || !enrichedPeer)
58141
58717
  return null;
58142
- if (blockedUidSet.has(enrichedPeer.uid) || hiddenPeerUidSet.has(enrichedPeer.uid))
58718
+ if (blockedUidSet.has(enrichedPeer.uid))
58143
58719
  return null;
58144
58720
  if (!discoveredPeerUids.has(enrichedPeer.uid)) {
58145
58721
  discoveredPeerUids.add(enrichedPeer.uid);
@@ -58198,37 +58774,14 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
58198
58774
  if (requestedKey !== blockedUidsKey)
58199
58775
  continue;
58200
58776
  blockedPeers = requestedUids.map((uid) => nextByUid.get(uid)).filter((peer) => peer?.uid);
58777
+ for (const peer of blockedPeers)
58778
+ discoveredPeerUids.add(peer.uid);
58201
58779
  blockedPeersReady = true;
58202
- rebuild({ projection: true });
58780
+ rebuild({ base: true });
58203
58781
  return blockedPeers;
58204
58782
  }
58205
58783
  return blockedPeers;
58206
58784
  }
58207
- function dropPeer(peer) {
58208
- const uid = peerUid(peer);
58209
- if (!uid)
58210
- return;
58211
- const discovered = discoveredPeerUids.has(uid);
58212
- if (!hiddenPeerUidSet.has(uid)) {
58213
- hiddenPeerUidSet.add(uid);
58214
- }
58215
- if (discovered) {
58216
- discoveredPeerUids.delete(uid);
58217
- }
58218
- if (typeof peer === "object" && peer?.uid) {
58219
- blockedPeers = [...blockedPeers.filter((item) => item?.uid !== uid), peer];
58220
- }
58221
- rebuild(discovered ? { base: true } : { projection: true });
58222
- }
58223
- function restorePeer(peer) {
58224
- const uid = peerUid(peer);
58225
- if (!uid)
58226
- return;
58227
- hiddenPeerUidSet.delete(uid);
58228
- blockedPeers = blockedPeers.filter((item) => item?.uid !== uid);
58229
- discoveredPeerUids.add(uid);
58230
- rebuild({ base: true });
58231
- }
58232
58785
  snapshot = makeSnapshot();
58233
58786
  return {
58234
58787
  getSnapshot() {
@@ -60972,7 +61525,7 @@ function compareRecentTransfer(a, b) {
60972
61525
  return String(b?.id || "").localeCompare(String(a?.id || ""));
60973
61526
  }
60974
61527
  function sameTransfer(left, right) {
60975
- return left?.id === right?.id && left?.status === right?.status && txCreatedMs(left) === txCreatedMs(right) && txUpdatedMs(left) === txUpdatedMs(right) && left?.totalValue === right?.totalValue && left?.type === right?.type && left?.transferDirection === right?.transferDirection && left?.senderIdentityPublicKey === right?.senderIdentityPublicKey && left?.receiverIdentityPublicKey === right?.receiverIdentityPublicKey && left?.bitcoinAddress === right?.bitcoinAddress && left?.bitcoinTxid === right?.bitcoinTxid && left?.lightningAddress === right?.lightningAddress;
61528
+ return left?.id === right?.id && left?.status === right?.status && txCreatedMs(left) === txCreatedMs(right) && txUpdatedMs(left) === txUpdatedMs(right) && left?.totalValue === right?.totalValue && left?.tokenIdentifier === right?.tokenIdentifier && left?.amountUnits === right?.amountUnits && left?.network === right?.network && left?.type === right?.type && left?.transferDirection === right?.transferDirection && left?.senderIdentityPublicKey === right?.senderIdentityPublicKey && left?.receiverIdentityPublicKey === right?.receiverIdentityPublicKey && left?.bitcoinAddress === right?.bitcoinAddress && left?.bitcoinTxid === right?.bitcoinTxid && left?.lightningAddress === right?.lightningAddress;
60976
61529
  }
60977
61530
  function sameTransfers(a = [], b = []) {
60978
61531
  if (a === b) {
@@ -61030,7 +61583,7 @@ function compactTransfer(tx, { incoming = false, bitcoinAddress = null, bitcoinT
61030
61583
  senderIdentityPublicKey: hexKey(tx.senderIdentityPublicKey),
61031
61584
  receiverIdentityPublicKey: hexKey(tx.receiverIdentityPublicKey),
61032
61585
  status: enumName(tx.status, TRANSFER_STATUS_BY_CODE),
61033
- totalValue: satValue(tx.totalValue),
61586
+ ...type === "TOKEN_TRANSFER" ? { tokenIdentifier: tx.tokenIdentifier, amountUnits: tx.amountUnits, network: tx.network } : { totalValue: satValue(tx.totalValue) },
61034
61587
  createdTime: tx.createdTime,
61035
61588
  updatedTime: tx.updatedTime,
61036
61589
  type,
@@ -61264,32 +61817,6 @@ function mergeTransferPageForWallet(currentTransfers = [], pageTransfers = [], w
61264
61817
  const page = compactTransfersForWallet(pageTransfers, walletPK);
61265
61818
  return mergeCompactedTransferPage(current, page, position);
61266
61819
  }
61267
- function mergeRecentSnapshotForWallet(currentTransfers = [], latestTransfers = [], walletPK) {
61268
- const currentById = new Map(currentTransfers.map((tx) => [String(tx?.id || ""), tx]));
61269
- const latest = mergeTransferPageForWallet([], latestTransfers, walletPK, "append").map((tx) => mergeTransferDetails(currentById.get(String(tx?.id || "")), tx));
61270
- if (!latest.length) {
61271
- return filterTransfersForWallet(currentTransfers, walletPK);
61272
- }
61273
- const latestIds = new Set(latest.map((tx) => String(tx.id)));
61274
- const latestById = new Map(latest.map((tx) => [String(tx.id), tx]));
61275
- const oldestLatestMs = getOldestTransferMs(latest);
61276
- const olderCurrent = filterTransfersForWallet(currentTransfers, walletPK).filter((tx) => {
61277
- const id = String(tx?.id || "");
61278
- if (latestIds.has(id)) {
61279
- const latestTx = latestById.get(id);
61280
- if (isPendingTransfer(latestTx) && !isPendingTransfer(tx)) {
61281
- return true;
61282
- }
61283
- return false;
61284
- }
61285
- if (isPendingTransfer(tx)) {
61286
- return false;
61287
- }
61288
- const createdMs = txCreatedMs(tx);
61289
- return oldestLatestMs == null || createdMs > 0 && createdMs < oldestLatestMs;
61290
- });
61291
- return mergeTransferPage(latest, olderCurrent, "append");
61292
- }
61293
61820
  function markClaimedTransfersCompleted(transfers = [], ids = []) {
61294
61821
  const claimedIds = new Set((Array.isArray(ids) ? ids : [ids]).map((id) => String(id || "")).filter(Boolean));
61295
61822
  if (!claimedIds.size)
@@ -61539,7 +62066,7 @@ async function refreshRequestedTransfers(owner, ids = []) {
61539
62066
  const resolvedIds = requestedTransfers.filter((transfer) => !isPendingTransfer(transfer)).map((transfer) => String(transfer.id));
61540
62067
  forgetPending(owner, resolvedIds);
61541
62068
  if (nextTransfers !== currentTransfers) {
61542
- await owner.snapshot.commitConfirmed(requestedTransfers.filter((transfer) => !isPendingTransfer(transfer)), { position: "prepend" });
62069
+ await owner.snapshot.commitHistory(overlayPending(owner, updates.map((transfer) => compactTransfer(transfer))), { position: "prepend" });
61543
62070
  if (!isCurrentGeneration(owner, generation))
61544
62071
  return [];
61545
62072
  owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), updates, owner.walletPK, "append"));
@@ -61657,8 +62184,7 @@ async function commitPendingReconciliation(owner, plan, result, generation) {
61657
62184
  const nextTransfers = result.updates.length ? overlayPending(owner, mergeTransferPageForWallet(current, result.updates, owner.walletPK, "append")) : current;
61658
62185
  forgetPending(owner, result.resolvedIds);
61659
62186
  if (nextTransfers !== current) {
61660
- const selectedIds = new Set(plan.selectedIds);
61661
- await owner.snapshot.commitConfirmed(nextTransfers.filter((transfer) => selectedIds.has(String(transfer?.id)) && !isPendingTransfer(transfer)), { position: "prepend" });
62187
+ await owner.snapshot.commitHistory(overlayPending(owner, result.updates.map((transfer) => compactTransfer(transfer))), { position: "prepend" });
61662
62188
  if (!isCurrentGeneration(owner, generation))
61663
62189
  return null;
61664
62190
  owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), result.updates, owner.walletPK, "append"));
@@ -61869,13 +62395,17 @@ function hydrateRecentHistory(owner) {
61869
62395
  return owner.hydratePromise;
61870
62396
  const generation = owner.generation;
61871
62397
  const startedAt = Date.now();
61872
- const request2 = owner.historyStore.query({ limit: WORKING_CONFIRMED_TRANSFER_LIMIT }).then((result) => {
61873
- if (!isCurrentGeneration2(owner, generation) || owner.snapshot.getTransfers().length || !result.transfers.length)
62398
+ const request2 = owner.historyStore.query({ limit: WORKING_CONFIRMED_TRANSFER_LIMIT }).then(async (result) => {
62399
+ const loadedIds = new Set(result.transfers.map((tx) => String(tx.id)));
62400
+ const missingPending = Object.keys(owner.historyStore.getSnapshot().projection?.pending || {}).filter((id) => !loadedIds.has(id));
62401
+ const pending = await Promise.all(missingPending.map((id) => owner.historyStore.getById(id)));
62402
+ const transfers = pending.length ? mergeTransferPage(result.transfers, pending.filter(Boolean), "prepend") : result.transfers;
62403
+ if (!isCurrentGeneration2(owner, generation) || owner.snapshot.getTransfers().length || !transfers.length)
61874
62404
  return false;
61875
62405
  owner.nextOffset = owner.historyStore.getSnapshot().nextOffset;
61876
- owner.snapshot.replace(result.transfers);
62406
+ owner.snapshot.replace(transfers);
61877
62407
  owner.snapshot.setReady();
61878
- markDone(owner.diag, "wallet.history.hydrate", startedAt, { count: result.transfers.length });
62408
+ markDone(owner.diag, "wallet.history.hydrate", startedAt, { count: transfers.length, pending: pendingTransfers(transfers).length });
61879
62409
  return true;
61880
62410
  }).catch((error) => {
61881
62411
  if (isCurrentGeneration2(owner, generation))
@@ -61934,13 +62464,12 @@ async function fetchHistoryPages(owner, options, generation) {
61934
62464
  completeOnStop: !!completeOnStop
61935
62465
  });
61936
62466
  try {
61937
- let stagedTransfers = owner.snapshot.getTransfers();
61938
62467
  let oldestFetchedMs = null;
61939
62468
  let stoppedAtKnownTransfer = false;
61940
62469
  while ((!owner.historyStore.getSnapshot().complete || force) && pages < maxPages) {
61941
62470
  if (!isCurrentGeneration2(owner, generation) || owner.backgroundSignal?.aborted === true)
61942
62471
  break;
61943
- if (!allHistory && Number.isFinite(sinceMs) && stagedHistoryCoverage(owner, stagedTransfers, oldestFetchedMs, sinceMs, force))
62472
+ if (!allHistory && Number.isFinite(sinceMs) && stagedHistoryCoverage(owner, owner.snapshot.getTransfers(), oldestFetchedMs, sinceMs, force))
61944
62473
  break;
61945
62474
  const offset = owner.nextOffset;
61946
62475
  const rawPage = await owner.readPage(TRANSFER_PAGE_LIMIT, offset, undefined, new Date(boundaryMs));
@@ -61951,12 +62480,11 @@ async function fetchHistoryPages(owner, options, generation) {
61951
62480
  const pageHitStopId = stopIds.size > 0 && page.transfers.some((transfer) => stopIds.has(String(transfer?.id)));
61952
62481
  if (page.transfers.length) {
61953
62482
  owner.pending.forgetResolvedFetched(page.transfers);
61954
- stagedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(stagedTransfers, page.transfers, owner.walletPK, "append"));
61955
62483
  const oldestPageMs = getOldestTransferMs(page.transfers);
61956
62484
  if (oldestPageMs != null)
61957
62485
  oldestFetchedMs = oldestFetchedMs == null ? oldestPageMs : Math.min(oldestFetchedMs, oldestPageMs);
61958
62486
  }
61959
- await owner.commitConfirmed(page.transfers, {
62487
+ await owner.commitHistory(page.transfers, {
61960
62488
  position: "append",
61961
62489
  nextOffset: page.nextOffset,
61962
62490
  boundaryMs,
@@ -61964,6 +62492,7 @@ async function fetchHistoryPages(owner, options, generation) {
61964
62492
  });
61965
62493
  if (!isCurrentGeneration2(owner, generation))
61966
62494
  break;
62495
+ owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), page.transfers, owner.walletPK, "append"));
61967
62496
  owner.nextOffset = Math.max(owner.nextOffset, page.nextOffset);
61968
62497
  await yieldToUi();
61969
62498
  if (pageHitStopId) {
@@ -61974,7 +62503,6 @@ async function fetchHistoryPages(owner, options, generation) {
61974
62503
  break;
61975
62504
  }
61976
62505
  if (isCurrentGeneration2(owner, generation)) {
61977
- owner.snapshot.replace(stagedTransfers);
61978
62506
  markDone(owner.diag, label, startedAt, {
61979
62507
  pages,
61980
62508
  loaded: owner.snapshot.getTransfers().length,
@@ -62071,13 +62599,12 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
62071
62599
  deltaAfterMs = Math.max(0, deltaState.newestMs - 1);
62072
62600
  deltaBeforeMs = Date.now();
62073
62601
  offset = 0;
62074
- await owner.commitConfirmed([], { deltaAfterMs, deltaBeforeMs, deltaNextOffset: 0 });
62602
+ await owner.commitHistory([], { deltaAfterMs, deltaBeforeMs, deltaNextOffset: 0 });
62075
62603
  if (!isCurrentGeneration2(owner, generation))
62076
62604
  return null;
62077
62605
  }
62078
62606
  const createdAfter = new Date(deltaAfterMs);
62079
62607
  const createdBefore = new Date(deltaBeforeMs);
62080
- let stagedTransfers = owner.snapshot.getTransfers();
62081
62608
  let rawCount = 0;
62082
62609
  let pages = 0;
62083
62610
  let complete = false;
@@ -62089,10 +62616,7 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
62089
62616
  rawCount += page.transfers.length;
62090
62617
  pages += 1;
62091
62618
  complete = !page.hasMore;
62092
- if (page.transfers.length) {
62093
- stagedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(stagedTransfers, page.transfers, walletKey, "prepend"));
62094
- }
62095
- await owner.commitConfirmed(page.transfers, {
62619
+ await owner.commitHistory(page.transfers, {
62096
62620
  position: "prepend",
62097
62621
  deltaAfterMs: complete ? null : deltaAfterMs,
62098
62622
  deltaBeforeMs: complete ? null : deltaBeforeMs,
@@ -62100,6 +62624,8 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
62100
62624
  });
62101
62625
  if (!isCurrentGeneration2(owner, generation))
62102
62626
  return null;
62627
+ owner.pending.forgetResolvedFetched(page.transfers);
62628
+ owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), page.transfers, walletKey, "prepend"));
62103
62629
  offset = page.nextOffset;
62104
62630
  if (complete)
62105
62631
  break;
@@ -62108,10 +62634,14 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
62108
62634
  if (!isCurrentGeneration2(owner, generation) || owner.backgroundSignal?.aborted === true)
62109
62635
  return null;
62110
62636
  const pending = includePending ? await owner.pending.query() : [];
62637
+ if (!isCurrentGeneration2(owner, generation))
62638
+ return null;
62639
+ if (pending.length)
62640
+ await owner.commitHistory(pending, { position: "prepend" });
62111
62641
  if (!isCurrentGeneration2(owner, generation))
62112
62642
  return null;
62113
62643
  owner.pending.remember(pending.map((transfer) => transfer.id));
62114
- const nextTransfers = pending.length ? mergeTransferPageForWallet(stagedTransfers, pending, walletKey, "prepend") : stagedTransfers;
62644
+ const nextTransfers = pending.length ? mergeTransferPageForWallet(owner.snapshot.getTransfers(), pending, walletKey, "prepend") : owner.snapshot.getTransfers();
62115
62645
  owner.snapshot.replace(nextTransfers);
62116
62646
  const visibleCount = Math.min(nextTransfers.length, Math.max(owner.snapshot.getTransfers().length, RECENT_TRANSFER_LIMIT));
62117
62647
  owner.snapshot.publish(nextTransfers.slice(0, visibleCount));
@@ -62200,7 +62730,7 @@ async function loadRecentTransfers(owner, options = {}, generation = owner.gener
62200
62730
  const loadedPendingIncoming = includePending ? owner.pending.claimCandidates(latestTransfers) : [];
62201
62731
  const latestIds = new Set(latestTransfers.map((transfer) => String(transfer?.id)).filter(Boolean));
62202
62732
  const reconcileIds = previousHistoryComplete ? [] : previousTransfers.filter((transfer) => transfer?.id && !latestIds.has(String(transfer.id)) && !isPendingTransfer(transfer)).map((transfer) => String(transfer.id));
62203
- await owner.commitConfirmed(page.transfers, {
62733
+ await owner.commitHistory([...pending, ...page.transfers], {
62204
62734
  position: "prepend",
62205
62735
  nextOffset: Math.max(owner.historyStore.getSnapshot().nextOffset, page.nextOffset),
62206
62736
  boundaryMs,
@@ -62208,12 +62738,11 @@ async function loadRecentTransfers(owner, options = {}, generation = owner.gener
62208
62738
  });
62209
62739
  if (!isCurrentGeneration2(owner, generation))
62210
62740
  return null;
62211
- const fetchedTransfers = page.hasMore ? mergeRecentSnapshotForWallet(previousTransfers, latestTransfers, owner.walletPK) : mergeTransferPageForWallet([], latestTransfers, owner.walletPK);
62212
- const committedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(owner.snapshot.getTransfers(), fetchedTransfers, owner.walletPK, "append"));
62213
62741
  if (!showLoading)
62214
62742
  await yieldToUi();
62215
62743
  if (!isCurrentGeneration2(owner, generation))
62216
62744
  return null;
62745
+ const committedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(owner.snapshot.getTransfers(), [...pending, ...page.transfers], owner.walletPK, "append"));
62217
62746
  owner.nextOffset = Math.max(owner.nextOffset, page.nextOffset);
62218
62747
  const committedHistory = owner.historyStore.getSnapshot();
62219
62748
  const visibleCount = Math.min(committedTransfers.length, Math.max(owner.snapshot.getTransfers().length, RECENT_TRANSFER_LIMIT));
@@ -62263,8 +62792,8 @@ function getRecentTransfers(owner, options = {}) {
62263
62792
  owner.recentPromise = request2;
62264
62793
  return request2;
62265
62794
  }
62266
- function createTransferHistorySync({ walletPK, historyStore, initialOffset = 0, snapshot, readPage, commitConfirmed, pending, waitForWork, backgroundSignal = null, isClosed, diag = null } = {}) {
62267
- if (!walletPK || !historyStore || !snapshot || typeof readPage !== "function" || typeof commitConfirmed !== "function" || !pending || typeof waitForWork !== "function" || typeof isClosed !== "function") {
62795
+ function createTransferHistorySync({ walletPK, historyStore, initialOffset = 0, snapshot, readPage, commitHistory, pending, waitForWork, backgroundSignal = null, isClosed, diag = null } = {}) {
62796
+ if (!walletPK || !historyStore || !snapshot || typeof readPage !== "function" || typeof commitHistory !== "function" || !pending || typeof waitForWork !== "function" || typeof isClosed !== "function") {
62268
62797
  throw new Error("transfer history sync requires history, snapshot, pending, and read ports");
62269
62798
  }
62270
62799
  const owner = {
@@ -62272,7 +62801,7 @@ function createTransferHistorySync({ walletPK, historyStore, initialOffset = 0,
62272
62801
  historyStore,
62273
62802
  snapshot,
62274
62803
  readPage,
62275
- commitConfirmed,
62804
+ commitHistory,
62276
62805
  pending,
62277
62806
  waitForWork,
62278
62807
  backgroundSignal,
@@ -62362,13 +62891,20 @@ function compareTransfers(left, right) {
62362
62891
  const delta = txCreatedMs(right) - txCreatedMs(left);
62363
62892
  return delta || transferId(right).localeCompare(transferId(left));
62364
62893
  }
62894
+ function mergeHistoryTransfer(current, next) {
62895
+ if (!current)
62896
+ return next;
62897
+ if (!isPendingTransfer(current) && isPendingTransfer(next) || txUpdatedMs(next) < txUpdatedMs(current))
62898
+ return current;
62899
+ return { ...current, ...next };
62900
+ }
62365
62901
  function normalizeTransfers(transfers, walletPK) {
62366
62902
  const byId = new Map;
62367
62903
  for (const transfer of Array.isArray(transfers) ? transfers : EMPTY_TRANSFERS) {
62368
62904
  const id = transferId(transfer);
62369
62905
  if (!id || !isVisibleTransfer(transfer) || !transferBelongsToWallet(transfer, walletPK))
62370
62906
  continue;
62371
- byId.set(id, jsonClone(transfer));
62907
+ byId.set(id, mergeHistoryTransfer(byId.get(id), jsonClone(transfer)));
62372
62908
  }
62373
62909
  return [...byId.values()].sort(compareTransfers);
62374
62910
  }
@@ -62393,7 +62929,8 @@ function emptyProjection() {
62393
62929
  days: {},
62394
62930
  hours: {},
62395
62931
  months: {},
62396
- peers: {}
62932
+ peers: {},
62933
+ pending: {}
62397
62934
  };
62398
62935
  }
62399
62936
  function cleanStats(value) {
@@ -62408,6 +62945,7 @@ function cleanStats(value) {
62408
62945
  }
62409
62946
  function cleanProjection(value) {
62410
62947
  const projection = emptyProjection();
62948
+ projection.pending = { ...value?.pending };
62411
62949
  projection.all = cleanStats(value?.all);
62412
62950
  for (const [key, stats] of Object.entries(value?.days || {}))
62413
62951
  projection.days[key] = cleanStats(stats);
@@ -62444,6 +62982,8 @@ function applyStats(stats, transfer, sign2, { excludeFunding = false } = {}) {
62444
62982
  const amount = Number(transfer?.totalValue) || 0;
62445
62983
  const incoming = isIncomingTransfer(transfer);
62446
62984
  stats.count += sign2;
62985
+ if (incoming && isPendingTransfer(transfer))
62986
+ return;
62447
62987
  stats.vol += sign2 * amount;
62448
62988
  if (incoming) {
62449
62989
  stats.received += sign2 * amount;
@@ -62462,6 +63002,11 @@ function normalizeStats(stats) {
62462
63002
  return stats;
62463
63003
  }
62464
63004
  function applyProjection(projection, transfer, sign2) {
63005
+ const id = transferId(transfer);
63006
+ if (sign2 > 0 && isPendingTransfer(transfer))
63007
+ projection.pending[id] = txCreatedMs(transfer);
63008
+ else
63009
+ delete projection.pending[id];
62465
63010
  const { day, hour, month } = timeKeys(transfer);
62466
63011
  projection.days[day] ||= emptyStats();
62467
63012
  projection.hours[hour] ||= emptyStats();
@@ -62547,11 +63092,12 @@ function normalizeManifest(value, walletPK) {
62547
63092
  projection: cleanProjection(value.projection)
62548
63093
  };
62549
63094
  }
62550
- function repositorySnapshot(manifest, ready, syncing = false) {
63095
+ function repositorySnapshot(manifest, ready, syncing = false, head = EMPTY_TRANSFERS) {
62551
63096
  return Object.freeze({
62552
63097
  walletPK: manifest.walletPK,
62553
63098
  ready,
62554
63099
  syncing,
63100
+ head,
62555
63101
  count: manifest.count,
62556
63102
  complete: manifest.complete,
62557
63103
  bitcoinOldestMs: manifest.bitcoinOldestMs,
@@ -62599,8 +63145,14 @@ function buildSegmentCommitPlan({ manifest, incoming, removedIds, mappings, oldS
62599
63145
  const incomingById = new Map(incoming.map((transfer) => [transferId(transfer), transfer]));
62600
63146
  const working = new Map;
62601
63147
  for (const [segmentId, segmentTransfers] of oldSegments) {
62602
- for (const transfer of segmentTransfers)
62603
- previousById.set(transferId(transfer), transfer);
63148
+ for (const transfer of segmentTransfers) {
63149
+ const id = transferId(transfer);
63150
+ previousById.set(id, transfer);
63151
+ const next2 = incomingById.get(id);
63152
+ if (!next2)
63153
+ continue;
63154
+ incomingById.set(id, mergeHistoryTransfer(transfer, next2));
63155
+ }
62604
63156
  const next = segmentTransfers.filter((transfer) => !removedIds.has(transferId(transfer))).map((transfer) => incomingById.get(transferId(transfer)) || transfer);
62605
63157
  working.set(segmentId, normalizeTransfers(next, walletPK));
62606
63158
  }
@@ -62816,6 +63368,15 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
62816
63368
  }
62817
63369
  return rememberSegment(id, transfers);
62818
63370
  };
63371
+ const readHead = async () => {
63372
+ const transfers = [];
63373
+ for (const segment of manifest.segments) {
63374
+ transfers.push(...await readSegment(segment.id));
63375
+ if (transfers.length >= WALLET_HISTORY_HEAD_SIZE)
63376
+ break;
63377
+ }
63378
+ return transfers.length ? Object.freeze(transfers.slice(0, WALLET_HISTORY_HEAD_SIZE)) : EMPTY_TRANSFERS;
63379
+ };
62819
63380
  const readIdMappings = async (ids, generation = manifest.generation) => {
62820
63381
  const grouped = new Map;
62821
63382
  for (const id of ids) {
@@ -62860,7 +63421,7 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
62860
63421
  if (!wrote)
62861
63422
  throw new Error("wallet history checkpoint write failed");
62862
63423
  manifest = normalizeManifest(nextManifest, requestedWalletPK);
62863
- snapshot = repositorySnapshot(manifest, true);
63424
+ snapshot = repositorySnapshot(manifest, true, false, await readHead());
62864
63425
  emit(listeners);
62865
63426
  };
62866
63427
  const pruneSegments = async () => {
@@ -62878,6 +63439,13 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
62878
63439
  };
62879
63440
  const prunePromise = pruneSegments();
62880
63441
  await prunePromise;
63442
+ const initialHead = await readHead().catch(async (error) => {
63443
+ if (!recoveryPromise)
63444
+ throw error;
63445
+ await recoveryPromise;
63446
+ return EMPTY_TRANSFERS;
63447
+ });
63448
+ snapshot = repositorySnapshot(manifest, true, false, initialHead);
62881
63449
  const readCommitSource = async (incoming, removedIds, position) => {
62882
63450
  const ids = [...new Set([...incoming.map(transferId), ...removedIds])];
62883
63451
  const { mappings, known, buckets: idBuckets } = await readIdMappings(ids);
@@ -62949,7 +63517,9 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
62949
63517
  if (!wrote)
62950
63518
  throw new Error("wallet history checkpoint write failed");
62951
63519
  manifest = normalizeManifest(nextManifest, requestedWalletPK);
62952
- snapshot = repositorySnapshot(manifest, true);
63520
+ for (const record of plan.newSegmentRecords)
63521
+ rememberSegment(record.descriptor.id, record.transfers);
63522
+ snapshot = repositorySnapshot(manifest, true, false, await readHead());
62953
63523
  emit(listeners);
62954
63524
  } else {
62955
63525
  for (const record of plan.newSegmentRecords) {
@@ -62976,6 +63546,10 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
62976
63546
  await prunePromise;
62977
63547
  const incoming = normalizeTransfers(transfers, requestedWalletPK);
62978
63548
  const removedIds = new Set((Array.isArray(options.removedIds) ? options.removedIds : []).map(String).filter(Boolean));
63549
+ for (const transfer of transfers) {
63550
+ if (transfer?.id && transferBelongsToWallet(transfer, requestedWalletPK) && !isVisibleTransfer(transfer))
63551
+ removedIds.add(String(transfer.id));
63552
+ }
62979
63553
  const bitcoinTimes = incoming.filter((tx) => !tx.tokenIdentifier).map(txCreatedMs);
62980
63554
  if (bitcoinTimes.length)
62981
63555
  options = { ...options, bitcoinOldestMs: Math.min(manifest.bitcoinOldestMs ?? Infinity, ...bitcoinTimes) };
@@ -63009,13 +63583,13 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
63009
63583
  commitPage(transfers, options = {}) {
63010
63584
  if (corruptionDetected)
63011
63585
  return Promise.reject(new Error("wallet history cache recovery in progress"));
63012
- snapshot = repositorySnapshot(manifest, true, true);
63586
+ snapshot = repositorySnapshot(manifest, true, true, snapshot.head);
63013
63587
  emit(listeners);
63014
63588
  const run2 = () => commit(transfers, options);
63015
63589
  const result = writeTail.then(run2, run2);
63016
63590
  writeTail = result.catch(() => {}).finally(() => {
63017
63591
  if (!closed) {
63018
- snapshot = repositorySnapshot(manifest, true, false);
63592
+ snapshot = repositorySnapshot(manifest, true, false, snapshot.head);
63019
63593
  emit(listeners);
63020
63594
  }
63021
63595
  });
@@ -63239,11 +63813,12 @@ function createWalletHistoryStore() {
63239
63813
  }
63240
63814
  });
63241
63815
  }
63242
- var MANIFEST_PART = "wallet-history-manifest", SEGMENT_PREFIX = "whs", ID_INDEX_PREFIX = "whi", PEER_INDEX_PREFIX = "whp", REPOSITORY_VERSION = 2, SEGMENT_SIZE = 100, SEGMENT_CACHE_LIMIT = 5, ID_BUCKET_COUNT = 4096, PEER_BUCKET_COUNT = 256, EMPTY_TRANSFERS;
63816
+ var MANIFEST_PART = "wallet-history-manifest", SEGMENT_PREFIX = "whs", ID_INDEX_PREFIX = "whi", PEER_INDEX_PREFIX = "whp", REPOSITORY_VERSION = 2, SEGMENT_SIZE = 100, WALLET_HISTORY_HEAD_SIZE, SEGMENT_CACHE_LIMIT = 5, ID_BUCKET_COUNT = 4096, PEER_BUCKET_COUNT = 256, EMPTY_TRANSFERS;
63243
63817
  var init_historyrepository = __esm(() => {
63244
63818
  init_core();
63245
63819
  init_time();
63246
63820
  init_tx();
63821
+ WALLET_HISTORY_HEAD_SIZE = SEGMENT_SIZE;
63247
63822
  EMPTY_TRANSFERS = Object.freeze([]);
63248
63823
  });
63249
63824
 
@@ -63316,9 +63891,9 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63316
63891
  const snapshot = transferStore.getSnapshot();
63317
63892
  return lowerText(snapshot.walletPK) === walletKey && snapshot.ready === true;
63318
63893
  };
63319
- const commitConfirmedHistoryPage = (pageTransfers, options = {}) => {
63320
- const confirmed = compactTransfersForWallet(pageTransfers, walletKey).filter((transfer) => !isPendingTransfer(transfer));
63321
- return historyStore.commitPage(confirmed, options);
63894
+ const commitHistoryPage = (pageTransfers, options = {}) => {
63895
+ const updates = pageTransfers.map((transfer) => compactTransfer(transfer)).filter((transfer) => transfer && transferBelongsToWallet(transfer, walletKey));
63896
+ return historyStore.commitPage(updates, options);
63322
63897
  };
63323
63898
  const forgetPendingLookupIds = (ids = []) => pendingReconciliation.forget(ids);
63324
63899
  const rememberPendingTransferIds = (ids = []) => pendingReconciliation.remember(ids);
@@ -63354,9 +63929,6 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63354
63929
  if (closed)
63355
63930
  return current;
63356
63931
  let ownedTransfers = owned ? nextTransfers : filterTransfersForWallet(nextTransfers, walletKey);
63357
- if (current.length && ownedTransfers.length && ownedTransfers.length < current.length) {
63358
- ownedTransfers = mergeTransferPageForWallet(ownedTransfers, current, walletKey, "append");
63359
- }
63360
63932
  ownedTransfers = boundedWorkingTransfers(applyTransferStatusOverlays(ownedTransfers));
63361
63933
  const committed = sameTransfers(current, ownedTransfers) ? current : ownedTransfers;
63362
63934
  transferStore.setHistory({ transfers: committed, walletPK: walletKey });
@@ -63493,7 +64065,7 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63493
64065
  return [];
63494
64066
  const nextTransfers = markClaimedTransfersCompleted(current, knownIds);
63495
64067
  if (nextTransfers !== current) {
63496
- commitConfirmedHistoryPage(nextTransfers.filter((transfer) => knownIds.includes(String(transfer?.id))), { position: "prepend" });
64068
+ commitHistoryPage(nextTransfers.filter((transfer) => knownIds.includes(String(transfer?.id))), { position: "prepend" });
63497
64069
  commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
63498
64070
  setTxReadyValue(true);
63499
64071
  }
@@ -63518,7 +64090,7 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63518
64090
  snapshot: {
63519
64091
  getTransfers: currentTransfers,
63520
64092
  replace: (transfers) => commitTransferSnapshot(transfers, { publish: true, owned: true }),
63521
- commitConfirmed: commitConfirmedHistoryPage,
64093
+ commitHistory: commitHistoryPage,
63522
64094
  setReady: () => setTxReadyValue(true)
63523
64095
  },
63524
64096
  reads: {
@@ -63542,7 +64114,7 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63542
64114
  setLoading: setIsTxLoading
63543
64115
  },
63544
64116
  readPage: fetchTransfersPage,
63545
- commitConfirmed: commitConfirmedHistoryPage,
64117
+ commitHistory: commitHistoryPage,
63546
64118
  pending: {
63547
64119
  query: getPendingTransfersSnapshot,
63548
64120
  isQuerying: () => !!pendingQueryPromiseRef.current,
@@ -63599,7 +64171,10 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63599
64171
  const current = currentTransfers();
63600
64172
  const nextTransfers = mergeTransferPageForWallet(current, incomingTransfers, walletKey, "prepend");
63601
64173
  if (nextTransfers !== current) {
63602
- commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
64174
+ await commitHistoryPage(incomingTransfers, { position: "prepend" });
64175
+ if (closed)
64176
+ return [];
64177
+ commitTransferSnapshot(mergeTransferPageForWallet(currentTransfers(), incomingTransfers, walletKey, "prepend"), { publish: true, owned: true });
63603
64178
  setTxReadyValue(true);
63604
64179
  }
63605
64180
  }
@@ -63641,10 +64216,8 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63641
64216
  forgetPendingLookupIds(resolvedUpdateIds);
63642
64217
  }
63643
64218
  }
63644
- const current = currentTransfers();
63645
- const nextTransfers = mergeTransferPageForWallet(current, updates, walletKey, "prepend");
63646
- await commitConfirmedHistoryPage(updates, { position: "prepend" });
63647
- commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
64219
+ await commitHistoryPage(updates, { position: "prepend" });
64220
+ commitTransferSnapshot(mergeTransferPageForWallet(currentTransfers(), updates, walletKey, "prepend"), { publish: true, owned: true });
63648
64221
  setTxReadyValue(true);
63649
64222
  markDone(diag, "wallet.claimedTxs", startedAt2, { found: true, count: updates.length });
63650
64223
  return updates;
@@ -63669,10 +64242,8 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
63669
64242
  forgetPendingLookupIds([compact2.id]);
63670
64243
  }
63671
64244
  }
63672
- const current = currentTransfers();
63673
- const nextTransfers = mergeTransferPageForWallet(current, [compact2], walletKey, "prepend");
63674
- await commitConfirmedHistoryPage([compact2], { position: "prepend" });
63675
- commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
64245
+ await commitHistoryPage([compact2], { position: "prepend" });
64246
+ commitTransferSnapshot(mergeTransferPageForWallet(currentTransfers(), [compact2], walletKey, "prepend"), { publish: true, owned: true });
63676
64247
  setTxReadyValue(true);
63677
64248
  markDone(diag, "wallet.claimedTx", startedAt, { found: true });
63678
64249
  return compact2;
@@ -64520,27 +65091,21 @@ var init_portfolio = __esm(() => {
64520
65091
  });
64521
65092
 
64522
65093
  // ../../core/wallet/txlist.js
64523
- async function readTxListPage(pending, queryHistory, { offset = 0, limit = 100 } = {}) {
65094
+ async function readTxListPage(queryHistory, { offset = 0, limit = 100 } = {}) {
64524
65095
  const start2 = Math.max(0, Math.floor(offset));
64525
65096
  const count = Math.max(1, Math.min(500, Math.floor(limit)));
64526
- const head = pending.slice(start2, start2 + count);
64527
- const remaining = count - head.length;
64528
- const history = remaining ? await queryHistory({ offset: Math.max(0, start2 - pending.length), limit: remaining }) : { transfers: [] };
64529
- return { offset: start2, transfers: [...head, ...history.transfers] };
64530
- }
64531
- async function findTxListPeriod(pending, history, queryHistory, { sinceMs, untilMs, hourly }) {
64532
- const pendingIndex = pending.findIndex((tx) => tx.createdMs >= sinceMs && tx.createdMs < untilMs);
64533
- if (pendingIndex >= 0)
64534
- return pendingIndex;
65097
+ const history = await queryHistory({ offset: start2, limit: count });
65098
+ return { offset: start2, transfers: history.transfers };
65099
+ }
65100
+ async function findTxListPeriod(history, queryHistory, { sinceMs, hourly }) {
64535
65101
  const key = (hourly ? dayHourKey : dayKey)(new Date(sinceMs));
64536
65102
  const buckets = history.projection[hourly ? "hours" : "days"];
64537
65103
  const newer = Object.entries(buckets).reduce((count, [period, stats]) => count + (period > key ? stats.count : 0), 0);
64538
65104
  if (buckets[key]?.count)
64539
- return pending.length + newer;
65105
+ return newer;
64540
65106
  const offset = Math.max(0, newer - 1);
64541
65107
  const { transfers } = await queryHistory({ offset, limit: 2 });
64542
- const candidates = pending.map((tx, index2) => ({ tx, index: index2 }));
64543
- transfers.forEach((tx, index2) => candidates.push({ tx, index: pending.length + offset + index2 }));
65108
+ const candidates = transfers.map((tx, index2) => ({ tx, index: offset + index2 }));
64544
65109
  candidates.sort((a, b) => Math.abs(a.tx.createdMs - sinceMs) - Math.abs(b.tx.createdMs - sinceMs));
64545
65110
  return candidates[0]?.index ?? -1;
64546
65111
  }
@@ -64639,14 +65204,14 @@ function mergeTxSearchResults(loaded, found, limit = TX_SEARCH_DEFAULT_LIMIT) {
64639
65204
  }
64640
65205
  return [...byId.values()].sort(byRecentTx).slice(0, max2);
64641
65206
  }
64642
- function getDailySeries({ days, transactions, aggregatedData, balance, historyComplete, oldestMs, cache }) {
65207
+ function getDailySeries({ days, transactions, aggregatedData, balance, historyComplete, oldestMs, cache, nowMs: nowMs2 = Date.now() }) {
64643
65208
  if (!transactions.length || balance == null)
64644
65209
  return [];
64645
65210
  const cacheKey = `daily-${days}`;
64646
- if (cache.has(cacheKey)) {
64647
- return cache.get(cacheKey);
64648
- }
64649
- const today = new Date;
65211
+ const clock = Math.floor(nowMs2 / 60000);
65212
+ if (cache.get(cacheKey)?.clock === clock)
65213
+ return cache.get(cacheKey).series;
65214
+ const today = new Date(nowMs2);
64650
65215
  today.setHours(0, 0, 0, 0);
64651
65216
  const series = [];
64652
65217
  let runningBalance = Number(balance);
@@ -64657,84 +65222,44 @@ function getDailySeries({ days, transactions, aggregatedData, balance, historyCo
64657
65222
  const d = new Date(today);
64658
65223
  d.setDate(d.getDate() - i2);
64659
65224
  const key = dayKey(d);
64660
- series.push({ date: key, balance: runningBalance });
65225
+ const end = new Date(d);
65226
+ end.setDate(end.getDate() + 1);
65227
+ series.push({ date: key, timestamp: i2 === 0 ? nowMs2 : end.getTime(), now: i2 === 0, balance: runningBalance });
64661
65228
  const dayData = aggregatedData.dayMap.get(key);
64662
65229
  if (dayData) {
64663
65230
  runningBalance -= dayData.net;
64664
65231
  }
64665
65232
  }
64666
65233
  const result = series.reverse();
64667
- cache.set(cacheKey, result);
65234
+ cache.set(cacheKey, { clock, series: result });
64668
65235
  return result;
64669
65236
  }
64670
- function getHourlySeriesForData({ hours, prefix = "today", transactions, aggregatedData, balance, historyComplete, oldestMs, cache }) {
65237
+ function getHourlySeriesForData({ hours, prefix = "today", transactions, aggregatedData, balance, historyComplete, oldestMs, cache, nowMs: nowMs2 = Date.now() }) {
64671
65238
  if (!transactions.length || balance == null)
64672
65239
  return [];
64673
65240
  const cacheKey = `hourly-${hours}-${prefix}`;
64674
- if (cache.has(cacheKey)) {
64675
- return cache.get(cacheKey);
64676
- }
64677
- const now = new Date;
64678
- const series = [];
65241
+ const clock = Math.floor(nowMs2 / 60000);
65242
+ if (cache.get(cacheKey)?.clock === clock)
65243
+ return cache.get(cacheKey).series;
65244
+ const now = new Date(nowMs2);
65245
+ const series = [{ hour: "now", timestamp: nowMs2, now: true, balance: Number(balance) }];
64679
65246
  let runningBalance = Number(balance);
64680
- const today = dayKey(now);
64681
- const yesterday = dayKey(new Date(now.getTime() - DAY_MS));
65247
+ const today = new Date(now);
65248
+ today.setHours(0, 0, 0, 0);
64682
65249
  const coveredHours = historyComplete ? hours : coveredUnitsSince(oldestMs, HOUR_MS, now.getTime());
64683
65250
  const actualHours = Math.min(hours, coveredHours);
64684
- if (prefix === "today") {
64685
- const currentHour = now.getHours();
64686
- const maxHour = Math.min(actualHours - 1, currentHour);
64687
- for (let i2 = 0;i2 <= maxHour; i2++) {
64688
- const key = `${today}-${hourKey(i2)}`;
64689
- const hourData = aggregatedData.hourMap.get(key);
64690
- if (hourData) {
64691
- runningBalance -= hourData.net;
64692
- }
64693
- }
64694
- for (let i2 = 0;i2 <= maxHour; i2++) {
64695
- const hour = hourKey(i2);
64696
- series.push({ hour, balance: runningBalance });
64697
- const key = `${today}-${hour}`;
64698
- const hourData = aggregatedData.hourMap.get(key);
64699
- if (hourData) {
64700
- runningBalance += hourData.net;
64701
- }
64702
- }
64703
- series.push({ hour: "now", balance: Number(balance) });
64704
- } else if (prefix === "24h") {
64705
- const currentHour = now.getHours();
64706
- const firstOffset = Math.max(0, 24 - actualHours);
64707
- for (let i2 = 0;i2 <= currentHour; i2++) {
64708
- const key = `${today}-${hourKey(i2)}`;
64709
- const hourData = aggregatedData.hourMap.get(key);
64710
- if (hourData)
64711
- runningBalance -= hourData.net;
64712
- }
64713
- for (let i2 = Math.max(currentHour + 1, firstOffset);i2 < 24; i2++) {
64714
- const key = `${yesterday}-${hourKey(i2)}`;
64715
- const hourData = aggregatedData.hourMap.get(key);
64716
- if (hourData)
64717
- runningBalance -= hourData.net;
64718
- }
64719
- for (let i2 = Math.max(currentHour + 1, firstOffset);i2 < 24; i2++) {
64720
- const hour = hourKey(i2);
64721
- series.push({ hour, balance: runningBalance });
64722
- const key = `${yesterday}-${hour}`;
64723
- const hourData = aggregatedData.hourMap.get(key);
64724
- if (hourData)
64725
- runningBalance += hourData.net;
64726
- }
64727
- for (let i2 = firstOffset > currentHour ? firstOffset : 0;i2 <= currentHour; i2++) {
64728
- const hour = hourKey(i2);
64729
- series.push({ hour, balance: runningBalance });
64730
- const key = `${today}-${hour}`;
64731
- const hourData = aggregatedData.hourMap.get(key);
64732
- if (hourData)
64733
- runningBalance += hourData.net;
64734
- }
64735
- }
64736
- cache.set(cacheKey, series);
64737
- return series;
65251
+ const endHour = new Date(now);
65252
+ endHour.setMinutes(0, 0, 0);
65253
+ for (let offset = 0;offset < actualHours; offset++) {
65254
+ const date = new Date(endHour.getTime() - offset * HOUR_MS);
65255
+ if (prefix === "today" && date < today)
65256
+ break;
65257
+ runningBalance -= aggregatedData.hourMap.get(dayHourKey(date))?.net || 0;
65258
+ series.push({ hour: hourKey(date), timestamp: date.getTime(), balance: runningBalance });
65259
+ }
65260
+ const result = series.reverse();
65261
+ cache.set(cacheKey, { clock, series: result });
65262
+ return result;
64738
65263
  }
64739
65264
  function getTxsInRangeFrom(sortedTransactions, timeRange) {
64740
65265
  if (!sortedTransactions.length)
@@ -64921,11 +65446,8 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
64921
65446
  transfers: result.transfers.map((transfer) => getVisibleWalletTransfer(transfer, sources.walletPK)).filter(Boolean)
64922
65447
  };
64923
65448
  }
64924
- function pendingList() {
64925
- return readCurrentAggregatedData(sources.walletPK).sortedTxs.filter((tx) => tx.pending);
64926
- }
64927
65449
  function getTxListPage(options) {
64928
- return readTxListPage(pendingList(), queryHistory, options);
65450
+ return readTxListPage(queryHistory, options);
64929
65451
  }
64930
65452
  async function findTxListPeriod2(period) {
64931
65453
  const { sinceMs, untilMs } = period;
@@ -64943,7 +65465,7 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
64943
65465
  return -1;
64944
65466
  if (!covered())
64945
65467
  throw new Error("transaction history is unavailable for this date");
64946
- return findTxListPeriod(pendingList(), historyStore.getSnapshot(), queryHistory, period);
65468
+ return findTxListPeriod(historyStore.getSnapshot(), queryHistory, period);
64947
65469
  }
64948
65470
  async function getHistoryById(txId, tokenIdentifier) {
64949
65471
  const { walletPK, lookupTxById } = sources;
@@ -64963,31 +65485,17 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
64963
65485
  const peerKey = lowerText(peerPK);
64964
65486
  const limit = Math.max(1, Math.min(500, Math.floor(Number(options.limit) || 100)));
64965
65487
  const offset = Math.max(0, Math.floor(Number(options.offset) || 0));
64966
- const pendingData = getPeerDataFromTransfers(transferStore.getSnapshot().transfers.filter(isPendingTransfer), sources.walletPK, peerKey);
64967
- const pending = pendingData.txs;
64968
- const pendingPage = pending.slice(offset, offset + limit);
64969
- const historyOffset = Math.max(0, offset - pending.length);
64970
- const historyLimit = Math.max(0, limit - pendingPage.length);
64971
- const result = historyLimit ? await queryHistory({ ...options, peerPK: peerKey, offset: historyOffset, limit: historyLimit }) : { transfers: [], cursor: null };
65488
+ const result = await queryHistory({ ...options, peerPK: peerKey, offset, limit });
64972
65489
  const storedStats = historyStore.getSnapshot().projection?.peers?.[peerKey];
64973
- const confirmedStats = {
65490
+ const stats = {
64974
65491
  sent: Number(storedStats?.sent) || 0,
64975
65492
  received: Number(storedStats?.received) || 0,
64976
65493
  net: Number(storedStats?.net) || 0,
64977
65494
  vol: Number(storedStats?.vol) || 0,
64978
65495
  cnt: Number(storedStats?.count) || 0
64979
65496
  };
64980
- const pendingStats = pendingData.stats || { sent: 0, received: 0, net: 0, vol: 0, cnt: 0 };
64981
- const stats = confirmedStats.cnt || pendingStats.cnt ? {
64982
- sent: confirmedStats.sent + pendingStats.sent,
64983
- received: confirmedStats.received + pendingStats.received,
64984
- net: confirmedStats.net + pendingStats.net,
64985
- vol: confirmedStats.vol + pendingStats.vol,
64986
- cnt: confirmedStats.cnt + pendingStats.cnt
64987
- } : null;
64988
- const txs = [...pendingPage, ...result.transfers];
64989
- const total = confirmedStats.cnt + pending.length;
64990
- return { ...result, transfers: txs, txs, stats, total, hasMore: offset + txs.length < total };
65497
+ const txs = result.transfers;
65498
+ return { ...result, txs, stats: stats.cnt ? stats : null, total: stats.cnt, hasMore: offset + txs.length < stats.cnt };
64991
65499
  }
64992
65500
  async function getHistorySummary(timeRange = "all-time") {
64993
65501
  const result = await historyStore.summarize({ sinceMs: getTxRangeStartMs(timeRange) });
@@ -65043,31 +65551,16 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
65043
65551
  }
65044
65552
  return [...combined.values()].sort(byRecentTx).slice(0, limit);
65045
65553
  }
65046
- function addCashSeries(series, hourly, prefix) {
65047
- const history = historyStore.getSnapshot();
65048
- const tokens = sources.tokens;
65554
+ function addCashSeries(series, hourly, history, tokens) {
65049
65555
  const coverage = !tokens.length || history.tokenComplete;
65050
- const now = new Date;
65051
65556
  const buckets = history.projection[hourly ? "hours" : "days"];
65052
65557
  return series.map((point) => {
65053
- let timestamp;
65054
- if (hourly) {
65055
- timestamp = new Date(now);
65056
- if (point.hour !== "now") {
65057
- const hour = Number(point.hour.slice(0, 2));
65058
- timestamp.setHours(hour, 0, 0, 0);
65059
- if (prefix === "24h" && hour > now.getHours())
65060
- timestamp.setDate(timestamp.getDate() - 1);
65061
- }
65062
- } else {
65063
- timestamp = new Date(`${point.date}T00:00:00`);
65064
- timestamp.setDate(timestamp.getDate() + 1);
65065
- }
65066
- const known = coverage || history.tokenOldestMs != null && timestamp.getTime() > history.tokenOldestMs;
65558
+ const timestamp = new Date(point.timestamp);
65559
+ const known = point.now || coverage || history.tokenOldestMs != null && timestamp.getTime() > history.tokenOldestMs;
65067
65560
  const key = (hourly ? dayHourKey : dayKey)(timestamp);
65068
65561
  const deltas = {};
65069
- if (point.hour !== "now")
65070
- for (const [bucketKey, stats] of Object.entries(buckets)) {
65562
+ if (!point.now)
65563
+ for (const [bucketKey, stats] of Object.entries(buckets || {})) {
65071
65564
  if (bucketKey < key)
65072
65565
  continue;
65073
65566
  for (const [id, units] of Object.entries(stats.tokens || {}))
@@ -65077,14 +65570,14 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
65077
65570
  });
65078
65571
  }
65079
65572
  function buildSnapshot() {
65080
- const { balance, walletPK, oldestTxMs, txReady, isTxLoading, ensureTxCoverage, loadMoreTxs } = sources;
65573
+ const { balance, tokens, walletPK, oldestTxMs, txReady, isTxLoading, ensureTxCoverage, loadMoreTxs } = sources;
65081
65574
  const storeSnapshot = transferStore.getSnapshot();
65082
65575
  const historySnapshot = historyStore.getSnapshot();
65083
65576
  observedStoreSnapshot = storeSnapshot;
65084
65577
  observedHistorySnapshot = historySnapshot;
65085
65578
  const transfers = storeSnapshot.transfers;
65086
65579
  const aggregatedData = deferAggregation ? EMPTY_AGG : readCurrentAggregatedData(walletPK);
65087
- const projectedData = projectionData(historySnapshot);
65580
+ const projectedData = historySnapshot.walletPK === walletPK ? projectionData(historySnapshot) : null;
65088
65581
  const transactions = aggregatedData.enrichedTxs;
65089
65582
  const sortedTransactions = aggregatedData.sortedTxs;
65090
65583
  const readLiveTransfers = () => deferAggregation ? transferStore.getSnapshot().transfers : transfers;
@@ -65154,16 +65647,17 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
65154
65647
  cache: seriesCache.current
65155
65648
  });
65156
65649
  };
65157
- const getHistorySeries = (days) => addCashSeries(getDailySeries({
65650
+ const getHistorySeries = (days, nowMs2 = Date.now()) => !projectedData ? [] : addCashSeries(getDailySeries({
65158
65651
  days,
65159
65652
  transactions: projectedData.enrichedTxs,
65160
65653
  aggregatedData: projectedData,
65161
65654
  balance,
65162
65655
  historyComplete: historySnapshot.complete,
65163
65656
  oldestMs: historySnapshot.bitcoinOldestMs,
65164
- cache: historySeriesCache.current
65165
- }), false);
65166
- const getHistoryHourlySeries = (hours, prefix = "today") => addCashSeries(getHourlySeriesForData({
65657
+ cache: historySeriesCache.current,
65658
+ nowMs: nowMs2
65659
+ }), false, historySnapshot, tokens);
65660
+ const getHistoryHourlySeries = (hours, prefix = "today", nowMs2 = Date.now()) => !projectedData ? [] : addCashSeries(getHourlySeriesForData({
65167
65661
  hours,
65168
65662
  prefix,
65169
65663
  transactions: projectedData.enrichedTxs,
@@ -65171,8 +65665,9 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
65171
65665
  balance,
65172
65666
  historyComplete: historySnapshot.complete,
65173
65667
  oldestMs: historySnapshot.bitcoinOldestMs,
65174
- cache: historySeriesCache.current
65175
- }), true, prefix);
65668
+ cache: historySeriesCache.current,
65669
+ nowMs: nowMs2
65670
+ }), true, historySnapshot, tokens);
65176
65671
  const getTxById = (txId) => getVisibleTransferById(readLiveTransfers(), walletPK, txId, transferByIdRef);
65177
65672
  const getPeerTxs = (peerPK) => peerPK ? readPeerData(peerPK).txs : EMPTY_TXS2;
65178
65673
  const getPeerStats = (peerPK) => peerPK ? readPeerData(peerPK).stats : null;
@@ -65205,7 +65700,7 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
65205
65700
  queryHistory,
65206
65701
  getTxListPage,
65207
65702
  findTxListPeriod: findTxListPeriod2,
65208
- txListCount: historySnapshot.count + transfers.filter((tx) => isPendingTransfer(tx) && isVisibleTransfer(tx) && transferBelongsToWallet(tx, walletPK)).length,
65703
+ txListCount: historySnapshot.walletPK === walletPK ? historySnapshot.count : 0,
65209
65704
  getTxById,
65210
65705
  searchTxs,
65211
65706
  getPeerTxs,
@@ -65985,8 +66480,7 @@ function openAccount(options = {}) {
65985
66480
  getPeerProfile: getChatPeerProfile,
65986
66481
  refreshPeerProfile: refreshChatPeerProfile,
65987
66482
  resolvePeerProfile: resolveChatPeerProfile,
65988
- maintenance: messageMaintenance,
65989
- resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
66483
+ maintenance: messageMaintenance
65990
66484
  }, chatSources());
65991
66485
  const calls = createCallSession({
65992
66486
  cloud,
@@ -66013,11 +66507,11 @@ function openAccount(options = {}) {
66013
66507
  function chatSources(sessionOverride = chatSession) {
66014
66508
  const currentUser = state.user || {};
66015
66509
  const session = sessionOverride;
66016
- const cachedBlacklist = session?.localCache?.read?.()?.blacklist;
66017
66510
  return {
66018
66511
  uid: currentUser.uid || "",
66019
- blocked: currentUser.blockedReady ? currentUser.blocked : cachedBlacklist || [],
66020
- blockedReady: currentUser.blockedReady || Array.isArray(cachedBlacklist),
66512
+ blocked: currentUser.blocked,
66513
+ blockedPending: currentUser.blockedPending,
66514
+ blockedReady: currentUser.blockedReady,
66021
66515
  online: state.online,
66022
66516
  chatPK: session?.chatPK || currentUser.chatPK || "",
66023
66517
  chatSigningPK: session?.chatSigningPK || currentUser.chatSigningPK || "",
@@ -66050,13 +66544,12 @@ function openAccount(options = {}) {
66050
66544
  }
66051
66545
  function syncPeers() {
66052
66546
  const currentUser = state.user || {};
66053
- const cachedBlacklist = state.session?.localCache?.read?.()?.blacklist;
66054
66547
  const chatSnapshot = chat.getSnapshot();
66055
66548
  peers.setSources({
66056
66549
  chatPeers: chatSnapshot.peers,
66057
66550
  chats: chatSnapshot.chats,
66058
- blocked: currentUser.blockedReady ? currentUser.blocked : cachedBlacklist || [],
66059
- blockedReady: currentUser.blockedReady || Array.isArray(cachedBlacklist),
66551
+ blocked: currentUser.blocked,
66552
+ blockedReady: currentUser.blockedReady,
66060
66553
  chatPK: state.session?.chatPK || currentUser.chatPK || "",
66061
66554
  chatSigningPK: state.session?.chatSigningPK || currentUser.chatSigningPK || "",
66062
66555
  walletPeers: wallet.getPeers(),
@@ -66341,8 +66834,9 @@ function openAccount(options = {}) {
66341
66834
  hasVault: !!vault,
66342
66835
  fromCache: !!info.fromCache
66343
66836
  });
66837
+ const currentVault = state.vault && vault && equalBytes(state.vault, vault) ? state.vault : vault || null;
66344
66838
  emit2({
66345
- vault: vault || null,
66839
+ vault: currentVault,
66346
66840
  vaultFromCache: false,
66347
66841
  vaultReady: state.vaultReady || !info.fromCache,
66348
66842
  vaultError: info.fromCache ? state.vaultError : null
@@ -66452,7 +66946,7 @@ function openAccount(options = {}) {
66452
66946
  function syncBootstrap() {
66453
66947
  if (closed || !state.session || state.session.closed || !serverReady())
66454
66948
  return;
66455
- if (state.user.blockedReady) {
66949
+ if (state.user.blockedReady && !state.user.blockedPending?.length) {
66456
66950
  const cache = state.session.localCache;
66457
66951
  const blocked = state.user.blocked;
66458
66952
  if (JSON.stringify(cache?.read?.()?.blacklist) !== JSON.stringify(blocked)) {
@@ -66592,6 +67086,7 @@ function openAccount(options = {}) {
66592
67086
  });
66593
67087
  if (!isCurrent())
66594
67088
  throw new Error("account changed during unlock");
67089
+ user.restoreBlocked({ uid, blocked: session.localCache?.read?.()?.blacklist });
66595
67090
  chatSession = session;
66596
67091
  chat.setSources(chatSources());
66597
67092
  unlockStage = "launching";
@@ -66786,15 +67281,21 @@ function openAccount(options = {}) {
66786
67281
  if (!observedVault)
66787
67282
  throw new Error("vault not ready");
66788
67283
  const isCurrent = () => !closed && !operationBarrier.blocksOperations && state.uid === uid && state.session === session && state.vault === observedVault && !session.closed && cloud.auth.user?.uid === uid;
67284
+ markDiag(diag, "account.delete.phase", { phase: "verify-vault" });
66789
67285
  const verified = await vaultCrypto.verifyVaultPassword(observedVault, password);
66790
67286
  if (!verified)
66791
67287
  throw new Error("incorrect password");
66792
67288
  if (!isCurrent())
66793
67289
  throw new Error("account changed during deletion");
67290
+ await waitForOnlineDeletion(isCurrent);
67291
+ markDiag(diag, "account.delete.phase", { phase: "retire-chats" });
66794
67292
  const chats = await chat.getSnapshot().deleteAllChats();
66795
67293
  if (!isCurrent())
66796
67294
  throw new Error("account changed during deletion");
67295
+ await waitForOnlineDeletion(isCurrent);
67296
+ markDiag(diag, "account.delete.phase", { phase: "delete-account" });
66797
67297
  await cloud.user.delete();
67298
+ markDiag(diag, "account.delete.phase", { phase: "clear-local-state" });
66798
67299
  try {
66799
67300
  await localCache?.clear?.();
66800
67301
  } catch (error) {
@@ -66818,6 +67319,31 @@ function openAccount(options = {}) {
66818
67319
  finishOperation();
66819
67320
  }
66820
67321
  }
67322
+ function waitForOnlineDeletion(isCurrent) {
67323
+ if (!isCurrent())
67324
+ return Promise.reject(new Error("account changed during deletion"));
67325
+ if (state.online)
67326
+ return Promise.resolve();
67327
+ return new Promise((resolve, reject) => {
67328
+ const finish = (error) => {
67329
+ clearTimeout(timer);
67330
+ listeners.delete(check);
67331
+ if (error)
67332
+ reject(error);
67333
+ else
67334
+ resolve();
67335
+ };
67336
+ const check = () => {
67337
+ if (!isCurrent())
67338
+ finish(new Error("account changed during deletion"));
67339
+ else if (state.online)
67340
+ finish();
67341
+ };
67342
+ const timer = setTimeout(() => finish(new Error("couldnt connect to server")), ACCOUNT_CONNECTION_UNAVAILABLE_MS);
67343
+ listeners.add(check);
67344
+ check();
67345
+ });
67346
+ }
66821
67347
  async function switchNetwork(network) {
66822
67348
  if (state.lockState !== "unlocked") {
66823
67349
  throw new Error("unlock the vault before switching networks");
@@ -66947,6 +67473,7 @@ var init_account = __esm(() => {
66947
67473
  init_peers2();
66948
67474
  init_support();
66949
67475
  init_wallet();
67476
+ init_utils2();
66950
67477
  });
66951
67478
 
66952
67479
  // ../../core/passkey.js
@@ -68574,7 +69101,6 @@ var init_listen = __esm(() => {
68574
69101
  init_messages();
68575
69102
  init_presentation();
68576
69103
  init_waveform();
68577
- init_call();
68578
69104
  init_messages3();
68579
69105
  });
68580
69106
 
@@ -68722,7 +69248,7 @@ function parseUrl(value) {
68722
69248
  return null;
68723
69249
  }
68724
69250
  }
68725
- function readParams(params) {
69251
+ function readInviteParams(params) {
68726
69252
  const compact2 = compactInvite(params);
68727
69253
  if (compact2)
68728
69254
  return compact2;
@@ -68757,7 +69283,7 @@ function makeInvite(value = {}) {
68757
69283
  const toIsUsername = !!cleanUsername2(rawTo);
68758
69284
  const paymentKind2 = cleanedKind === invite.send || cleanedKind === invite.request;
68759
69285
  const receiver = value.r ?? value.walletPK ?? value.receiver ?? (paymentKind2 && !toIsUsername ? rawTo : null);
68760
- const data = readParams({
69286
+ const data = readInviteParams({
68761
69287
  kind,
68762
69288
  from: value.from ?? value.sender,
68763
69289
  to: rawTo,
@@ -68807,7 +69333,7 @@ function readInvite(raw) {
68807
69333
  return makeInvite({ kind: invite.join });
68808
69334
  if (url.pathname !== "/")
68809
69335
  return null;
68810
- return readParams(url.searchParams);
69336
+ return readInviteParams(url.searchParams);
68811
69337
  }
68812
69338
  var invite, inviteKinds, tokenKinds;
68813
69339
  var init_invite = __esm(() => {
@@ -69052,23 +69578,20 @@ function createRuntimeProductActions({
69052
69578
  const chatSnapshot = runtime.chat.getSnapshot();
69053
69579
  const result = await executeUserBlock({
69054
69580
  report: () => supportOwner.report(peer.uid),
69055
- block: () => chatSnapshot.retirePeerChat(peer.chatPK, {
69056
- onDeliveryRevoked: () => user.blockPeer(peer)
69581
+ block: () => user.blockPeer(peer, {
69582
+ beforeCommit: () => chatSnapshot.retirePeerChat(peer.chatPK)
69057
69583
  })
69058
69584
  });
69059
69585
  if (!result.blocked) {
69060
69586
  throw new Error(`block failed during private route retirement: ${result.blockError?.message || "unknown error"}`, { cause: result.blockError });
69061
69587
  }
69062
- runtime.peers.getSnapshot().dropPeer(peer);
69063
69588
  return { blocked: true, reported: result.reported, peer: projectPeer({ ...peer, blocked: true }) };
69064
69589
  },
69065
69590
  async unblock(value) {
69066
69591
  const runtime = await readyRuntime();
69067
69592
  const user = await getUser();
69068
69593
  const peer = await resolvePeer(value);
69069
- await runtime.chat.getSnapshot().retirePeerChat(peer.chatPK);
69070
- await user.unblockPeer(peer);
69071
- runtime.peers.getSnapshot().restorePeer(peer);
69594
+ await user.unblockPeer(peer, { beforeCommit: () => runtime.chat.getSnapshot().retirePeerChat(peer.chatPK) });
69072
69595
  return { blocked: false, peer: projectPeer({ ...peer, blocked: false }) };
69073
69596
  }
69074
69597
  };
@@ -69600,7 +70123,6 @@ function deriveMessageRouteProjection({
69600
70123
  conversationStateKey,
69601
70124
  deleteMessageDocs,
69602
70125
  chatActivity,
69603
- resolveActiveCall,
69604
70126
  epochId,
69605
70127
  exists,
69606
70128
  expireMessageBatch: expireMessageBatch2,
@@ -69707,7 +70229,6 @@ function deriveMessageRouteProjection({
69707
70229
  expireMessageBatch: expireMessageBatch2,
69708
70230
  deleteMessageDocs,
69709
70231
  chatActivity,
69710
- resolveActiveCall,
69711
70232
  pruneMessageCache: pruneMessageCache2
69712
70233
  },
69713
70234
  liveWindowKey,
@@ -70764,11 +71285,11 @@ function messageStateHasKey(state, key) {
70764
71285
  }
70765
71286
  return false;
70766
71287
  }
70767
- function shouldKeepInitialWindowHidden(currentlyHidden, hasOlder, messageGroups, pageSize) {
71288
+ function shouldKeepInitialWindowHidden(currentlyHidden, hasOlder, messageGroups) {
70768
71289
  if (!currentlyHidden || hasOlder !== true)
70769
71290
  return false;
70770
71291
  const visible = (messageGroups || []).reduce((total, messages) => total + countShowableRows(messages), 0);
70771
- return visible < Math.max(1, Math.floor(Number(pageSize) || 1));
71292
+ return visible === 0;
70772
71293
  }
70773
71294
  function queueRouteCachePrune(refs, messageCacheWriter, config, messages, options) {
70774
71295
  const {
@@ -71035,11 +71556,11 @@ function currentHydrationWindow(refs) {
71035
71556
  visible: countShowableRows(historyMessages) + countShowableRows(latestMessages)
71036
71557
  };
71037
71558
  }
71038
- function shouldGateInitialCache(refs, window2, routePageSize) {
71039
- return refs.initialHydratingCache.current && window2.count > 0 && window2.visible < routePageSize && refs.readMessageState().hasOlder && !refs.olderLoaded.current;
71559
+ function shouldGateInitialCache(refs, window2) {
71560
+ return refs.initialHydratingCache.current && window2.count > 0 && window2.visible === 0 && refs.readMessageState().hasOlder && !refs.olderLoaded.current;
71040
71561
  }
71041
71562
  function adoptCurrentRouteWindow(refs, config, window2) {
71042
- if (!refs.olderLoaded.current || window2.count <= 0)
71563
+ if (!refs.olderLoaded.current || window2.count <= 0 || refs.readMessageState().hasOlder)
71043
71564
  return false;
71044
71565
  refs.messageCachePartialHydrate.current = !!refs.readMessageState().hasOlder;
71045
71566
  config.setInitialHydratingCache(false);
@@ -71262,7 +71783,7 @@ function finishMessageCacheHydration(refs, config, hydrateTask) {
71262
71783
  refs.messageCacheHydratePromise.current = null;
71263
71784
  }
71264
71785
  const currentVisible = countShowableRows(refs.readMessageState().historyMessages) + countShowableRows(refs.readMessageState().latestMessages) + countShowableRows(refs.locals.current);
71265
- const cacheWindowCanReveal = refs.readMessageState()?.ready === true && currentVisible >= config.routePageSize && (refs.initialServerWindowApplied.current || !refs.messageCachePartialHydrate.current || refs.readMessageState().hasOlder !== true);
71786
+ const cacheWindowCanReveal = refs.readMessageState()?.ready === true && (currentVisible > 0 || refs.readMessageState().hasOlder !== true);
71266
71787
  if (!config.chatId || cacheWindowCanReveal || refs.routeActive.current !== true) {
71267
71788
  config.setInitialHydratingCache(false);
71268
71789
  }
@@ -71276,7 +71797,7 @@ function hydrateRouteMessageCache(refs, config) {
71276
71797
  refs.messageCacheHydrateKey.current = hydrateKey;
71277
71798
  const window2 = currentHydrationWindow(refs);
71278
71799
  if (window2.count > 0 && refs.initialHydratingCache.current) {
71279
- config.setInitialHydratingCache(shouldGateInitialCache(refs, window2, config.routePageSize));
71800
+ config.setInitialHydratingCache(shouldGateInitialCache(refs, window2));
71280
71801
  }
71281
71802
  if (adoptCurrentRouteWindow(refs, config, window2))
71282
71803
  return;
@@ -71523,7 +72044,7 @@ function applyResolvedBatch(config, refs, msgBatch, prepared, context) {
71523
72044
  fast: true
71524
72045
  });
71525
72046
  }
71526
- config.setInitialHydratingCache(shouldKeepInitialWindowHidden(refs.initialHydratingCache.current || !refs.initialServerWindowApplied.current, merged.nextHasOlder, [merged.nextHistory, merged.nextLive, refs.locals.current], config.routePageSize));
72047
+ config.setInitialHydratingCache(shouldKeepInitialWindowHidden(refs.initialHydratingCache.current || !refs.initialServerWindowApplied.current, merged.nextHasOlder, [merged.nextHistory, merged.nextLive, refs.locals.current]));
71527
72048
  if (context.applyingServerBatch)
71528
72049
  refs.initialServerWindowApplied.current = true;
71529
72050
  config.ackMessages(config.chatId, merged.nextBatchMessages);
@@ -72614,7 +73135,6 @@ function readRouteExpiry(current, chatId, options = {}) {
72614
73135
  const messages = current.rawMessages?.length ? current.rawMessages : current.messages;
72615
73136
  return analyzeMessageExpiry(messages, current.selfChatPublicKey, current.peerChatPublicKey, {
72616
73137
  now: options.now,
72617
- activeCallId: options.activeCallId,
72618
73138
  keepKeys: options.release ? new Set : current.keepKeys,
72619
73139
  memberChatPKs: current.memberChatPKs,
72620
73140
  memberStatesByEpoch: current.memberStatesByEpoch
@@ -72737,32 +73257,6 @@ function expireRouteMessages(current, chatId, options = {}, diag = null, data =
72737
73257
  return false;
72738
73258
  if (options.release === true)
72739
73259
  scheduleLoadedControlCompaction(current, chatId, diag, data);
72740
- if (options.release === true && typeof current.resolveActiveCall === "function" && ((current.rawMessages?.length) ? current.rawMessages : current.messages || []).some((message) => readCallMessage(message)?.event === "started")) {
72741
- const lease = messageMaintenance(current).claim("call-expiry", chatId);
72742
- if (lease) {
72743
- const work = Promise.resolve().then(() => {
72744
- lease.assertCurrent();
72745
- return current.resolveActiveCall(chatId).catch((error) => {
72746
- markError(diag, "chat.call.expiry", Date.now(), error);
72747
- return;
72748
- });
72749
- }).then((activeCallId) => {
72750
- lease.assertCurrent();
72751
- if (current.routeMemory?.isActive(chatId))
72752
- return;
72753
- const expired = readRouteExpiry(current, chatId, { ...options, activeCallId })?.expired || [];
72754
- if (!expired.length)
72755
- return;
72756
- hideExpiredRouteMessages(current, chatId, expired, diag, data);
72757
- return deleteExpiredMessages(current, chatId, expired, diag, data);
72758
- }).catch((error) => {
72759
- if (!isStaleMessageMaintenance(error))
72760
- markError(diag, "chat.call.expiry", Date.now(), error);
72761
- }).finally(() => lease.release());
72762
- messageMaintenance(current).track(work);
72763
- }
72764
- return true;
72765
- }
72766
73260
  const analysis = readRouteExpiry(current, chatId, { now: options.now, release: options.release === true });
72767
73261
  const expiredMessages = analysis?.expired || [];
72768
73262
  if (!expiredMessages.length) {
@@ -72785,7 +73279,6 @@ var init_expirycleanup = __esm(() => {
72785
73279
  init_maintenance();
72786
73280
  init_window();
72787
73281
  init_control();
72788
- init_call();
72789
73282
  });
72790
73283
 
72791
73284
  // ../../core/chat/messages/routerelease.js
@@ -74284,7 +74777,7 @@ function createRouteMessageHistory({
74284
74777
  return;
74285
74778
  }
74286
74779
  const before = countShowableRows(refs.readMessageState().historyMessages) + countShowableRows(refs.readMessageState().latestMessages) + countShowableRows(localsRef.current);
74287
- if (before >= routePageSize || routeActiveRef.current !== true) {
74780
+ if (before > 0 || routeActiveRef.current !== true) {
74288
74781
  setInitialHydratingCache(false);
74289
74782
  return;
74290
74783
  }
@@ -74294,7 +74787,7 @@ function createRouteMessageHistory({
74294
74787
  return;
74295
74788
  }
74296
74789
  const after = countShowableRows(refs.readMessageState().historyMessages) + countShowableRows(refs.readMessageState().latestMessages) + countShowableRows(localsRef.current);
74297
- if (!refs.readMessageState().hasOlder || after >= routePageSize || after <= before) {
74790
+ if (!refs.readMessageState().hasOlder || after > 0 || after <= before) {
74298
74791
  setInitialHydratingCache(false);
74299
74792
  }
74300
74793
  }).catch((error) => {
@@ -74852,7 +75345,6 @@ function deriveProjection(owner, input = owner.input) {
74852
75345
  conversationStateKey: input.conversationStateKey,
74853
75346
  deleteMessageDocs: input.deleteMessageDocs,
74854
75347
  chatActivity: input.chatActivity,
74855
- resolveActiveCall: input.resolveActiveCall,
74856
75348
  epochId: input.epochId,
74857
75349
  exists: state.exists,
74858
75350
  expireMessageBatch: input.messageRuntime.expireMessageBatch,
@@ -75113,7 +75605,6 @@ var init_routereconcile = __esm(() => {
75113
75605
  "conversationStateKey",
75114
75606
  "deleteMessageDocs",
75115
75607
  "chatActivity",
75116
- "resolveActiveCall",
75117
75608
  "epochId",
75118
75609
  "hasCurrentChat",
75119
75610
  "initialState",
@@ -75227,7 +75718,7 @@ function createMessageRouteState(initialInput) {
75227
75718
  yieldToMain: yieldToMain2
75228
75719
  } = routeStateOptions(initialInput);
75229
75720
  const messageStore = createMessageStore(messageStoreState(initialState, chatId, conversationStateKey));
75230
- const initialHydratingCache = !!chatId && (initialState?.ready !== true || initialState?.exists === true && initialState?.hasOlder === true);
75721
+ const initialHydratingCache = !!chatId && (initialState?.ready !== true || initialState?.exists === true && initialState?.hasOlder === true && countShowableRows(initialState.historyMessages) + countShowableRows(initialState.latestMessages) === 0);
75231
75722
  const refs = routeRefs(messageStore, {
75232
75723
  chatPreviewKey,
75233
75724
  foreground: foreground === true,
@@ -75375,6 +75866,7 @@ var init_routestate = __esm(() => {
75375
75866
  init_routerelease();
75376
75867
  init_routereconcile();
75377
75868
  init_async();
75869
+ init_history();
75378
75870
  ROUTE_STATE_DEFAULTS = Object.freeze({
75379
75871
  cancelSchedule: clearTimeout,
75380
75872
  chatPreviewKey: null,
@@ -76065,12 +76557,9 @@ function createRuntimeChatActions({
76065
76557
  throw new Error("chat id required");
76066
76558
  const runtime = await readyChatRuntime();
76067
76559
  let snapshot2 = runtime.chat.getSnapshot();
76068
- let chat = snapshot2.chats.find((item) => item?.id === id) || null;
76069
- while (!chat && snapshot2.hasMoreChats && !snapshot2.loadingMoreChats) {
76070
- await snapshot2.loadMoreChats();
76071
- snapshot2 = runtime.chat.getSnapshot();
76072
- chat = snapshot2.chats.find((item) => item?.id === id) || null;
76073
- }
76560
+ await snapshot2.ensureChat(id);
76561
+ snapshot2 = runtime.chat.getSnapshot();
76562
+ let chat = snapshot2.getOwnerChat(id);
76074
76563
  if (!chat && options.allowPending === true && snapshot2.pendingChatLaunch?.id === id) {
76075
76564
  chat = snapshot2.pendingChatLaunch.chat || null;
76076
76565
  }
@@ -76429,8 +76918,18 @@ function createRuntimeChatActions({
76429
76918
  if (!["accept", "reject"].includes(nextDecision)) {
76430
76919
  throw new Error("chat request decision must be accept or reject");
76431
76920
  }
76432
- const { runtime, chat } = await chatForId(chatId);
76433
- if (chat.messageRequest !== true)
76921
+ const runtime = await readyChatRuntime();
76922
+ const id = cleanText(chatId).toLowerCase();
76923
+ if (!/^[0-9a-f]{64}$/u.test(id))
76924
+ throw new Error("chat id required");
76925
+ let snapshot2 = runtime.chat.getSnapshot();
76926
+ let chat = snapshot2.chats.find((item) => item.id === id);
76927
+ while (!chat && snapshot2.hasMoreChats && !snapshot2.loadingMoreChats) {
76928
+ await snapshot2.loadMoreChats();
76929
+ snapshot2 = runtime.chat.getSnapshot();
76930
+ chat = snapshot2.chats.find((item) => item.id === id);
76931
+ }
76932
+ if (chat?.messageRequest !== true)
76434
76933
  throw new Error("message request not found");
76435
76934
  const resolved = await runtime.chat.getSnapshot().resolveChatRequest(chat.id, nextDecision);
76436
76935
  if (resolved !== true)
@@ -77388,7 +77887,7 @@ var package_default;
77388
77887
  var init_package2 = __esm(() => {
77389
77888
  package_default = {
77390
77889
  name: "veyl",
77391
- version: "0.77.0",
77890
+ version: "0.78.0",
77392
77891
  packageManager: "bun@1.4.0",
77393
77892
  private: true,
77394
77893
  license: "UNLICENSED",
@@ -78038,6 +78537,11 @@ class AccountRuntime {
78038
78537
  if (!this.session) {
78039
78538
  await this.unlockVault({ key: vaultSecret, saveKey: false });
78040
78539
  }
78540
+ const session = this.session;
78541
+ const ready = await waitForOwner(this.accountOwner, (state) => state.online || state.uid !== profile.uid || state.session !== session, options.timeoutMs);
78542
+ if (ready.uid !== profile.uid || ready.session !== session || session.closed) {
78543
+ throw new Error("account changed before deletion");
78544
+ }
78041
78545
  const profileName = profile.profile || "";
78042
78546
  const localCache = this.session?.localCache;
78043
78547
  const result = await this.accountOwner.delete({