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