@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/account.js CHANGED
@@ -16835,6 +16835,8 @@ var WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
16835
16835
  var WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
16836
16836
  var WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
16837
16837
  var BITCOIN_FEES_MAX_AGE_MS = 5 * 60000;
16838
+ var CALL_MAX_VIDEO_SOURCES = 4;
16839
+ var CALL_MAX_VIDEO_SOURCES_PER_PARTICIPANT = 1;
16838
16840
 
16839
16841
  // ../../core/calls/wire.js
16840
16842
  var CALL_REQUEST_MAX_BYTES = 192 * 1024;
@@ -17007,144 +17009,6 @@ function sameText(left, right) {
17007
17009
  return lowerText(left) === lowerText(right);
17008
17010
  }
17009
17011
 
17010
- // ../../core/utils/time.js
17011
- function timestampMs(value, fallback = null, options = {}) {
17012
- let ms = null;
17013
- if (typeof value?.toMillis === "function") {
17014
- ms = value.toMillis();
17015
- } else if (value instanceof Date) {
17016
- ms = value.getTime();
17017
- } else if (typeof value?.seconds === "number") {
17018
- ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
17019
- } else if (typeof value?._seconds === "number") {
17020
- ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
17021
- } else if (Number.isFinite(value)) {
17022
- ms = value;
17023
- } else if (options.parseString && typeof value === "string") {
17024
- const numberMs = Number(value);
17025
- ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
17026
- }
17027
- if (!Number.isFinite(ms) || options.positive && ms <= 0) {
17028
- return fallback;
17029
- }
17030
- return ms;
17031
- }
17032
- function timestampKey(value) {
17033
- if (value == null) {
17034
- return null;
17035
- }
17036
- return timestampMs(value, null) ?? String(value);
17037
- }
17038
- function makeTimestamp(ms) {
17039
- return {
17040
- toMillis() {
17041
- return ms;
17042
- },
17043
- toDate() {
17044
- return new Date(ms);
17045
- }
17046
- };
17047
- }
17048
- function twoDigits(value) {
17049
- return String(value).padStart(2, "0");
17050
- }
17051
- function dayKey(date) {
17052
- return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
17053
- }
17054
- function localDayKey(value) {
17055
- const ms = timestampMs(value, null, { parseString: true });
17056
- if (!Number.isFinite(ms))
17057
- return "";
17058
- return dayKey(new Date(ms));
17059
- }
17060
- function hourKey(dateOrHour) {
17061
- const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
17062
- return twoDigits(hour);
17063
- }
17064
- function dayHourKey(date) {
17065
- return `${dayKey(date)}-${hourKey(date)}`;
17066
- }
17067
- var MINUTE_MS2 = 60000;
17068
- var HOUR_MS2 = 60 * MINUTE_MS2;
17069
- function nextLocalDayStartMs(ms) {
17070
- const date = new Date(ms);
17071
- date.setDate(date.getDate() + 1);
17072
- date.setHours(0, 0, 0, 0);
17073
- return date.getTime();
17074
- }
17075
- function nextRowDateTimeRefreshMs(value, now = Date.now()) {
17076
- const ms = timestampMs(value, null, { parseString: true });
17077
- const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
17078
- if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
17079
- return null;
17080
- const age = nowMs2 - ms;
17081
- if (age < -MINUTE_MS2)
17082
- return ms - MINUTE_MS2;
17083
- if (age < MINUTE_MS2)
17084
- return ms + MINUTE_MS2;
17085
- if (age < HOUR_MS2)
17086
- return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
17087
- if (localDayKey(ms) === localDayKey(nowMs2))
17088
- return nextLocalDayStartMs(nowMs2);
17089
- return null;
17090
- }
17091
- function nextRelativeTimeRefreshMs(value, now = Date.now()) {
17092
- const ms = timestampMs(value, null, { parseString: true });
17093
- const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
17094
- if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
17095
- return null;
17096
- const age = Math.max(0, nowMs2 - ms);
17097
- if (age < 30000)
17098
- return ms + 30000;
17099
- if (age < 60 * 60000)
17100
- return ms + (Math.floor(age / 60000) + 1) * 60000;
17101
- if (age < 24 * 60 * 60000)
17102
- return ms + (Math.floor(age / (60 * 60000)) + 1) * 60 * 60000;
17103
- return ms + (Math.floor(age / (24 * 60 * 60000)) + 1) * 24 * 60 * 60000;
17104
- }
17105
-
17106
- // ../../core/chat/state.js
17107
- function makeCid() {
17108
- return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
17109
- }
17110
- function getMessageKey(message) {
17111
- return message?.cid || message?.id || null;
17112
- }
17113
- function getCidMs(cid) {
17114
- if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
17115
- return null;
17116
- }
17117
- const base = cid.slice(0, -6);
17118
- const ms = Number.parseInt(base, 36);
17119
- return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
17120
- }
17121
- function getMessageOrderMs(message) {
17122
- return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
17123
- }
17124
- function sortMessages(messages) {
17125
- return [...messages].sort((a, b) => {
17126
- const aMs = getMessageOrderMs(a);
17127
- const bMs = getMessageOrderMs(b);
17128
- if (aMs !== bMs) {
17129
- return aMs - bMs;
17130
- }
17131
- return String(a?.id || "").localeCompare(String(b?.id || ""));
17132
- });
17133
- }
17134
- function mergeMessages(...groups) {
17135
- const merged = new Map;
17136
- for (const group of groups) {
17137
- for (const message of group || []) {
17138
- const key = getMessageKey(message);
17139
- if (!key) {
17140
- continue;
17141
- }
17142
- merged.set(key, message);
17143
- }
17144
- }
17145
- return sortMessages([...merged.values()]);
17146
- }
17147
-
17148
17012
  // ../../core/chat/messages/call.js
17149
17013
  var CALL_MSG_TYPE = "call";
17150
17014
  function readCallMessage(message) {
@@ -17162,13 +17026,6 @@ function callMessageText(message) {
17162
17026
  const call = readCallMessage(message);
17163
17027
  return call ? `call ${call.event}` : "";
17164
17028
  }
17165
- function latestCallStartKey(messages) {
17166
- for (let index = messages.length - 1;index >= 0; index -= 1) {
17167
- if (readCallMessage(messages[index])?.event === "started")
17168
- return getMessageKey(messages[index]);
17169
- }
17170
- return "";
17171
- }
17172
17029
 
17173
17030
  // ../../core/chat/messages/types.js
17174
17031
  var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
@@ -17701,6 +17558,7 @@ var domains = Object.freeze({
17701
17558
  root: ROOT_DOMAIN,
17702
17559
  rootDev: `dev.${ROOT_DOMAIN}`,
17703
17560
  veyl: `veyl.${ROOT_DOMAIN}`,
17561
+ veylOpen: `open.veyl.${ROOT_DOMAIN}`,
17704
17562
  veylDev: `dev.veyl.${ROOT_DOMAIN}`,
17705
17563
  live: `live.veyl.${ROOT_DOMAIN}`,
17706
17564
  liveDev: `live.dev.veyl.${ROOT_DOMAIN}`,
@@ -17719,6 +17577,7 @@ var origins = Object.freeze({
17719
17577
  root: `https://${domains.root}`,
17720
17578
  rootDev: `https://${domains.rootDev}`,
17721
17579
  veyl: `https://${domains.veyl}`,
17580
+ veylOpen: `https://${domains.veylOpen}`,
17722
17581
  veylDev: `https://${domains.veylDev}`,
17723
17582
  veylDevWeb: getVeylDevWebOrigin(VEYL_DEV_WEB_PORT_MIN)
17724
17583
  });
@@ -17733,7 +17592,8 @@ var appDomains = Object.freeze([
17733
17592
  ]);
17734
17593
  var appLinkDomains = Object.freeze([
17735
17594
  domains.veyl,
17736
- domains.veylDev
17595
+ domains.veylDev,
17596
+ domains.veylOpen
17737
17597
  ]);
17738
17598
 
17739
17599
  // ../../core/network.js
@@ -17761,7 +17621,6 @@ function isAddressOnNetwork(address, network) {
17761
17621
 
17762
17622
  // ../../core/settings.js
17763
17623
  var SEND_ON_SCAN_ENABLED = false;
17764
- var WEB_LAYOUTS = ["floating", "sidebar"];
17765
17624
  var WEB_SIDEBAR_WIDTH = { default: 320, min: 64, max: 480 };
17766
17625
  var DEFAULT_WEB_CHAT_LAYOUT = Object.freeze({ open: null, width: null });
17767
17626
  var WALLET_NETWORKS = new Set([MAINNET_NETWORK, REGTEST_NETWORK]);
@@ -17774,7 +17633,6 @@ class SettingsValidationError extends Error {
17774
17633
  }
17775
17634
  var defaultSettings = {
17776
17635
  glass: true,
17777
- webLayout: "floating",
17778
17636
  sidebarWidth: WEB_SIDEBAR_WIDTH.default,
17779
17637
  walletChartHeight: null,
17780
17638
  webChatLayouts: [],
@@ -17889,9 +17747,6 @@ function normalizeSettings(settings, base = defaultSettings) {
17889
17747
  if (!MONEY_FORMATS.includes(next.moneyFormat)) {
17890
17748
  throw new SettingsValidationError("money-format", "bad moneyFormat");
17891
17749
  }
17892
- if (!WEB_LAYOUTS.includes(next.webLayout)) {
17893
- throw new SettingsValidationError("web-layout", "bad webLayout");
17894
- }
17895
17750
  if (typeof next.glass !== "boolean") {
17896
17751
  throw new SettingsValidationError("glass", "glass must be boolean");
17897
17752
  }
@@ -17913,7 +17768,7 @@ function normalizeSettings(settings, base = defaultSettings) {
17913
17768
  if (next.faceID !== null && typeof next.faceID !== "boolean") {
17914
17769
  throw new SettingsValidationError("face-id", "faceID must be boolean or null");
17915
17770
  }
17916
- return next;
17771
+ return Object.fromEntries(Object.keys(defaultSettings).map((key) => [key, next[key]]));
17917
17772
  }
17918
17773
  function normalizeCallAudio(patch, base = defaultSettings.callAudio) {
17919
17774
  if (patch !== undefined && (!patch || typeof patch !== "object" || Array.isArray(patch)))
@@ -19360,7 +19215,14 @@ async function openVaultAccountSession(vault, password, options = {}) {
19360
19215
  return Promise.reject(new Error("account session closed"));
19361
19216
  if (connecting)
19362
19217
  return connecting;
19218
+ let releaseBoot;
19363
19219
  connecting = (async () => {
19220
+ const walletStartedAt = Date.now();
19221
+ mark(diag, "vault.unlock.wallet.start", { source });
19222
+ const boot = startWalletBoot(bootWallet, mnemonicFromWalletEntropy(sessionWalletEntropy), currentUser, { network, diag });
19223
+ walletBoot = boot;
19224
+ releaseBoot = session.own(boot);
19225
+ boot.result.catch(() => {});
19364
19226
  const authStartedAt = Date.now();
19365
19227
  mark(diag, "vault.unlock.session.start", { source });
19366
19228
  await cloud.auth.user.getIdToken(true);
@@ -19394,11 +19256,6 @@ async function openVaultAccountSession(vault, password, options = {}) {
19394
19256
  requireCurrent(isCurrent);
19395
19257
  chatIdentitiesRegistered = true;
19396
19258
  }
19397
- const walletStartedAt = Date.now();
19398
- mark(diag, "vault.unlock.wallet.start", { source });
19399
- const boot = startWalletBoot(bootWallet, mnemonicFromWalletEntropy(sessionWalletEntropy), currentUser, { network, diag });
19400
- walletBoot = boot;
19401
- const releaseBoot = session.own(boot);
19402
19259
  const walletReady = boot.result.then(async (walletIdentity) => {
19403
19260
  if (session.closed)
19404
19261
  throw new Error("account session closed");
@@ -19430,6 +19287,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
19430
19287
  walletReady.catch(() => {});
19431
19288
  return { walletReady };
19432
19289
  })().catch((error) => {
19290
+ releaseBoot?.();
19433
19291
  connecting = null;
19434
19292
  throw error;
19435
19293
  });
@@ -19475,6 +19333,102 @@ function hasAgreement(user, contract = CURRENT_AGREEMENT) {
19475
19333
  return isAgreementAccepted(user?.agreement, contract);
19476
19334
  }
19477
19335
 
19336
+ // ../../core/utils/time.js
19337
+ function timestampMs(value, fallback = null, options = {}) {
19338
+ let ms = null;
19339
+ if (typeof value?.toMillis === "function") {
19340
+ ms = value.toMillis();
19341
+ } else if (value instanceof Date) {
19342
+ ms = value.getTime();
19343
+ } else if (typeof value?.seconds === "number") {
19344
+ ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
19345
+ } else if (typeof value?._seconds === "number") {
19346
+ ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
19347
+ } else if (Number.isFinite(value)) {
19348
+ ms = value;
19349
+ } else if (options.parseString && typeof value === "string") {
19350
+ const numberMs = Number(value);
19351
+ ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
19352
+ }
19353
+ if (!Number.isFinite(ms) || options.positive && ms <= 0) {
19354
+ return fallback;
19355
+ }
19356
+ return ms;
19357
+ }
19358
+ function timestampKey(value) {
19359
+ if (value == null) {
19360
+ return null;
19361
+ }
19362
+ return timestampMs(value, null) ?? String(value);
19363
+ }
19364
+ function makeTimestamp(ms) {
19365
+ return {
19366
+ toMillis() {
19367
+ return ms;
19368
+ },
19369
+ toDate() {
19370
+ return new Date(ms);
19371
+ }
19372
+ };
19373
+ }
19374
+ function twoDigits(value) {
19375
+ return String(value).padStart(2, "0");
19376
+ }
19377
+ function dayKey(date) {
19378
+ return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
19379
+ }
19380
+ function localDayKey(value) {
19381
+ const ms = timestampMs(value, null, { parseString: true });
19382
+ if (!Number.isFinite(ms))
19383
+ return "";
19384
+ return dayKey(new Date(ms));
19385
+ }
19386
+ function hourKey(dateOrHour) {
19387
+ const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
19388
+ return twoDigits(hour);
19389
+ }
19390
+ function dayHourKey(date) {
19391
+ return `${dayKey(date)}-${hourKey(date)}`;
19392
+ }
19393
+ var MINUTE_MS2 = 60000;
19394
+ var HOUR_MS2 = 60 * MINUTE_MS2;
19395
+ function nextLocalDayStartMs(ms) {
19396
+ const date = new Date(ms);
19397
+ date.setDate(date.getDate() + 1);
19398
+ date.setHours(0, 0, 0, 0);
19399
+ return date.getTime();
19400
+ }
19401
+ function nextRowDateTimeRefreshMs(value, now = Date.now()) {
19402
+ const ms = timestampMs(value, null, { parseString: true });
19403
+ const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
19404
+ if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
19405
+ return null;
19406
+ const age = nowMs2 - ms;
19407
+ if (age < -MINUTE_MS2)
19408
+ return ms - MINUTE_MS2;
19409
+ if (age < MINUTE_MS2)
19410
+ return ms + MINUTE_MS2;
19411
+ if (age < HOUR_MS2)
19412
+ return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
19413
+ if (localDayKey(ms) === localDayKey(nowMs2))
19414
+ return nextLocalDayStartMs(nowMs2);
19415
+ return null;
19416
+ }
19417
+ function nextRelativeTimeRefreshMs(value, now = Date.now()) {
19418
+ const ms = timestampMs(value, null, { parseString: true });
19419
+ const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
19420
+ if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
19421
+ return null;
19422
+ const age = Math.max(0, nowMs2 - ms);
19423
+ if (age < 30000)
19424
+ return ms + 30000;
19425
+ if (age < 60 * 60000)
19426
+ return ms + (Math.floor(age / 60000) + 1) * 60000;
19427
+ if (age < 24 * 60 * 60000)
19428
+ return ms + (Math.floor(age / (60 * 60000)) + 1) * 60 * 60000;
19429
+ return ms + (Math.floor(age / (24 * 60 * 60000)) + 1) * 24 * 60 * 60000;
19430
+ }
19431
+
19478
19432
  // ../../core/moderation.js
19479
19433
  function banUntilMs(ban) {
19480
19434
  if (!ban || typeof ban !== "object" || Array.isArray(ban) || ban.until == null) {
@@ -20003,6 +19957,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20003
19957
  let avatarPreview;
20004
19958
  let blockedSource = state.blocked;
20005
19959
  let blockedSet = new Set(blockedSource);
19960
+ const blockedChanges = new Map;
20006
19961
  let banTimer = null;
20007
19962
  let unsubscribeAuth = NOOP;
20008
19963
  let userWatches = [];
@@ -20016,11 +19971,27 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20016
19971
  function makeSnapshot() {
20017
19972
  const bans = banState(state.banned);
20018
19973
  const userBan = bans.full || bans.chat;
19974
+ const visibleBlocked = new Set(state.blocked);
19975
+ const blockedPending = [];
19976
+ for (const [uid, change] of blockedChanges) {
19977
+ if (change.blocked)
19978
+ visibleBlocked.add(uid);
19979
+ else
19980
+ visibleBlocked.delete(uid);
19981
+ if (!change.written)
19982
+ blockedPending.push(uid);
19983
+ }
19984
+ if (visibleBlocked.size !== blockedSet.size || [...visibleBlocked].some((uid) => !blockedSet.has(uid))) {
19985
+ blockedSet = visibleBlocked;
19986
+ }
19987
+ const blocked = snapshot?.blocked && snapshot.blocked.length === blockedSet.size && snapshot.blocked.every((uid) => blockedSet.has(uid)) ? snapshot.blocked : [...blockedSet];
20019
19988
  return {
20020
19989
  ...state,
20021
19990
  avatar: avatarPreview === undefined ? state.avatar : avatarPreview,
20022
19991
  network: activeNetwork,
20023
19992
  blockedSet,
19993
+ blocked,
19994
+ blockedPending: snapshot?.blockedPending && snapshot.blockedPending.length === blockedPending.length && snapshot.blockedPending.every((uid, index) => uid === blockedPending[index]) ? snapshot.blockedPending : blockedPending,
20024
19995
  chatBanned: bans.chatBanned,
20025
19996
  avatarBanned: bans.avatarBanned,
20026
19997
  chatBanUntil: userBan?.until ?? null,
@@ -20304,6 +20275,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20304
20275
  const authStartedAt = Date.now();
20305
20276
  const session = authSession + 1;
20306
20277
  authSession = session;
20278
+ blockedChanges.clear();
20307
20279
  closeUserWatches();
20308
20280
  markDiag(diag, "user.auth.state", { signedIn: !!authUser });
20309
20281
  if (!authUser) {
@@ -20403,6 +20375,20 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20403
20375
  setState((user) => ({ ...user, banned: null }));
20404
20376
  })));
20405
20377
  userWatches.push(cloud.user.blocked.watch(authUser.uid, current((blocked, info = {}) => {
20378
+ if (info.pending || info.fromCache && state.blockedReady)
20379
+ return;
20380
+ const confirmed = new Set(blocked);
20381
+ for (const [uid, change] of blockedChanges) {
20382
+ if (!change.written)
20383
+ continue;
20384
+ if (!info.fromCache && confirmed.has(uid) === change.blocked)
20385
+ blockedChanges.delete(uid);
20386
+ else if (state.blocked.includes(uid))
20387
+ confirmed.add(uid);
20388
+ else
20389
+ confirmed.delete(uid);
20390
+ }
20391
+ blocked = [...confirmed];
20406
20392
  setState((user) => {
20407
20393
  const blockedReady = user.blockedReady || !info.fromCache;
20408
20394
  if (user.blockedReady === blockedReady && user.blocked.length === blocked.length && user.blocked.every((id, index) => id === blocked[index])) {
@@ -20478,6 +20464,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20478
20464
  if (!started)
20479
20465
  return;
20480
20466
  authSession += 1;
20467
+ blockedChanges.clear();
20481
20468
  closeUserWatches();
20482
20469
  unsubscribeAuth();
20483
20470
  setState((user) => ({ ...user, authError: null, authSessionError: null, profileError: null, settingsError: null }));
@@ -20499,6 +20486,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20499
20486
  agreementStored = null;
20500
20487
  agreementOverride = null;
20501
20488
  agreementAcceptance = null;
20489
+ blockedChanges.clear();
20502
20490
  userUid = null;
20503
20491
  avatarFetch = { uid: null, key: null, promise: null };
20504
20492
  avatarPreview = undefined;
@@ -20507,25 +20495,63 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20507
20495
  blockedSet = new Set(blockedSource);
20508
20496
  snapshot = makeSnapshot();
20509
20497
  }
20510
- function blockPeer(peer) {
20498
+ function changePeerBlock(peer, blocked, { beforeCommit } = {}) {
20511
20499
  const nextPeerUid = peerUid(peer);
20512
20500
  const uid = cloud.auth.user?.uid;
20501
+ const session = authSession;
20513
20502
  if (!uid)
20514
20503
  throw new Error("auth");
20515
20504
  if (!nextPeerUid)
20516
20505
  throw new Error("peer uid required");
20517
- if (nextPeerUid === uid)
20506
+ if (blocked && nextPeerUid === uid)
20518
20507
  return;
20519
- return cloud.user.blocked.add(uid, nextPeerUid);
20508
+ const prior = blockedChanges.get(nextPeerUid);
20509
+ if (prior && prior.blocked === blocked)
20510
+ return prior.promise;
20511
+ const current = () => isCurrentUser(uid, session);
20512
+ const assertCurrent = () => {
20513
+ if (!current())
20514
+ throw new Error("account changed during block update");
20515
+ };
20516
+ const change = { blocked, written: false, promise: null };
20517
+ change.promise = Promise.resolve().then(async () => {
20518
+ try {
20519
+ await prior?.promise.catch(NOOP);
20520
+ assertCurrent();
20521
+ await beforeCommit?.();
20522
+ assertCurrent();
20523
+ const result = await cloud.user.blocked[blocked ? "add" : "remove"](uid, nextPeerUid);
20524
+ assertCurrent();
20525
+ change.written = true;
20526
+ if (blockedChanges.get(nextPeerUid) === change && state.blocked.includes(nextPeerUid) === blocked) {
20527
+ blockedChanges.delete(nextPeerUid);
20528
+ }
20529
+ const confirmed = new Set(state.blocked);
20530
+ if (blocked)
20531
+ confirmed.add(nextPeerUid);
20532
+ else
20533
+ confirmed.delete(nextPeerUid);
20534
+ setState((user) => ({ ...user, blocked: [...confirmed] }));
20535
+ return result;
20536
+ } catch (error) {
20537
+ if (current() && blockedChanges.get(nextPeerUid) === change) {
20538
+ blockedChanges.delete(nextPeerUid);
20539
+ if (prior?.written)
20540
+ blockedChanges.set(nextPeerUid, prior);
20541
+ setState(state, true);
20542
+ }
20543
+ throw error;
20544
+ }
20545
+ });
20546
+ blockedChanges.set(nextPeerUid, change);
20547
+ setState(state, true);
20548
+ return change.promise;
20520
20549
  }
20521
- function unblockPeer(peer) {
20522
- const nextPeerUid = peerUid(peer);
20523
- const uid = cloud.auth.user?.uid;
20524
- if (!uid)
20525
- throw new Error("auth");
20526
- if (!nextPeerUid)
20527
- throw new Error("peer uid required");
20528
- return cloud.user.blocked.remove(uid, nextPeerUid);
20550
+ function blockPeer(peer, options) {
20551
+ return changePeerBlock(peer, true, options);
20552
+ }
20553
+ function unblockPeer(peer, options) {
20554
+ return changePeerBlock(peer, false, options);
20529
20555
  }
20530
20556
  async function acceptAgreement(requestedAgreement = CURRENT_AGREEMENT) {
20531
20557
  const uid = cloud.auth.user?.uid;
@@ -20739,6 +20765,13 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20739
20765
  syncAvatar();
20740
20766
  return true;
20741
20767
  }
20768
+ function restoreBlocked({ uid, blocked }) {
20769
+ if (!started || !uid || uid !== state.uid || cloud.auth.user?.uid !== uid)
20770
+ return;
20771
+ if (state.blockedReady || !Array.isArray(blocked))
20772
+ return;
20773
+ setState((user) => ({ ...user, blocked: [...blocked], blockedReady: true }));
20774
+ }
20742
20775
  snapshot = makeSnapshot();
20743
20776
  return {
20744
20777
  getSnapshot() {
@@ -20764,6 +20797,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20764
20797
  previewAvatar,
20765
20798
  confirmAvatar,
20766
20799
  restoreBootstrap,
20800
+ restoreBlocked,
20767
20801
  retry
20768
20802
  };
20769
20803
  }
@@ -21463,6 +21497,48 @@ function readAccountBootstrap(value, uid) {
21463
21497
  };
21464
21498
  }
21465
21499
 
21500
+ // ../../core/chat/state.js
21501
+ function makeCid() {
21502
+ return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
21503
+ }
21504
+ function getMessageKey(message) {
21505
+ return message?.cid || message?.id || null;
21506
+ }
21507
+ function getCidMs(cid) {
21508
+ if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
21509
+ return null;
21510
+ }
21511
+ const base = cid.slice(0, -6);
21512
+ const ms = Number.parseInt(base, 36);
21513
+ return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
21514
+ }
21515
+ function getMessageOrderMs(message) {
21516
+ return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
21517
+ }
21518
+ function sortMessages(messages) {
21519
+ return [...messages].sort((a, b) => {
21520
+ const aMs = getMessageOrderMs(a);
21521
+ const bMs = getMessageOrderMs(b);
21522
+ if (aMs !== bMs) {
21523
+ return aMs - bMs;
21524
+ }
21525
+ return String(a?.id || "").localeCompare(String(b?.id || ""));
21526
+ });
21527
+ }
21528
+ function mergeMessages(...groups) {
21529
+ const merged = new Map;
21530
+ for (const group of groups) {
21531
+ for (const message of group || []) {
21532
+ const key = getMessageKey(message);
21533
+ if (!key) {
21534
+ continue;
21535
+ }
21536
+ merged.set(key, message);
21537
+ }
21538
+ }
21539
+ return sortMessages([...merged.values()]);
21540
+ }
21541
+
21466
21542
  // ../../core/chat/ids.js
21467
21543
  function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
21468
21544
  const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
@@ -24023,6 +24099,24 @@ async function openStateRecordsWithEvidence(epochState, records) {
24023
24099
  closeStateEpoch(opened);
24024
24100
  }
24025
24101
  }
24102
+ async function readChatMemberState(cloud, epochState, actorChatPK) {
24103
+ if (!chatEpochCapabilities(epochState).sharedReceipts)
24104
+ return null;
24105
+ const member = epochState?.manifest?.members?.find((value) => value.chatPK === actorChatPK);
24106
+ if (!member)
24107
+ return null;
24108
+ const opened = openStateEpoch(epochState);
24109
+ try {
24110
+ const slotId = deriveChatMemberStateSlot(opened.epoch, member.chatSigningPK);
24111
+ const record = await cloud.chat.memberState.readSlot(opened.epoch.epochId, slotId);
24112
+ if (!record || !stateRecordActive(record))
24113
+ return null;
24114
+ const state = await openChatMemberState(opened.epoch, record);
24115
+ return state.actor === actorChatPK ? stateRecordProjection(opened.epoch.epochId, record, state) : null;
24116
+ } finally {
24117
+ closeStateEpoch(opened);
24118
+ }
24119
+ }
24026
24120
  function watchChatMemberStateEvidence(cloud, epochState, onUpdate, onError) {
24027
24121
  if (!chatEpochCapabilities(epochState).sharedReceipts)
24028
24122
  return () => {};
@@ -25178,8 +25272,6 @@ function cleanChatPayment(message, { requireTx }) {
25178
25272
  } catch {
25179
25273
  return null;
25180
25274
  }
25181
- if (message.mode || message.transfers?.length !== 1)
25182
- return null;
25183
25275
  } else if (message.tokenIdentifier != null)
25184
25276
  return null;
25185
25277
  const transfers = cleanChatPaymentTransfers(message?.transfers, { requireTx, token });
@@ -25191,6 +25283,13 @@ function cleanChatPayment(message, { requireTx }) {
25191
25283
  if (transfers.length > 1 && !mode || rawMode && !mode)
25192
25284
  return null;
25193
25285
  const total = transfers.reduce((sum, transfer) => sum + BigInt(transfer.a), 0n);
25286
+ if (token) {
25287
+ try {
25288
+ tokenUnits(total);
25289
+ } catch {
25290
+ return null;
25291
+ }
25292
+ }
25194
25293
  if (!mode && (transfers.length !== 1 || BigInt(transfers[0].a) !== amount))
25195
25294
  return null;
25196
25295
  if (mode === GROUP_SEND_MODES.EACH && transfers.some((transfer) => BigInt(transfer.a) !== amount))
@@ -25216,14 +25315,14 @@ function makeChatPaymentPayload({ amountSats, amountUnits, tokenIdentifier, tran
25216
25315
  if (token) {
25217
25316
  tokenAsset(tokenIdentifier, paymentNetwork);
25218
25317
  tokenUnits(amount);
25219
- if (mode || transfers.length !== 1)
25220
- throw new Error("token payments require one direct recipient");
25221
25318
  }
25222
25319
  const rawMode = cleanText(mode);
25223
25320
  const groupMode = GROUP_PAYMENT_MODES.has(rawMode) ? rawMode : null;
25224
25321
  if (transfers.length > 1 && !groupMode || rawMode && !groupMode)
25225
25322
  throw new Error("chat payment mode required");
25226
25323
  const total = transfers.reduce((sum, transfer) => sum + BigInt(transfer.a), 0n);
25324
+ if (token)
25325
+ tokenUnits(total);
25227
25326
  if (!groupMode && (transfers.length !== 1 || BigInt(transfers[0].a) !== amount)) {
25228
25327
  throw new Error("direct payment amount must match its transfer");
25229
25328
  }
@@ -25260,8 +25359,6 @@ function chatPaymentMembersValid(message, payment) {
25260
25359
  if (sender && recipients.includes(sender))
25261
25360
  return false;
25262
25361
  const epochMembers = Array.isArray(message?.epochMemberChatPKs) ? new Set(message.epochMemberChatPKs.map((member) => cleanText(member).toLowerCase()).filter(Boolean)) : null;
25263
- if (message.tokenIdentifier != null && epochMembers?.size > 2)
25264
- return false;
25265
25362
  return !epochMembers?.size || recipients.every((recipient) => epochMembers.has(recipient));
25266
25363
  }
25267
25364
  function isChatPayment(message) {
@@ -25870,17 +25967,10 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
25870
25967
  const heldExpired = [];
25871
25968
  const shorten = [];
25872
25969
  let nextExpiryAt = null;
25873
- const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
25874
- const endedCalls = new Set(projectedMessages.filter(isServerConfirmedMsg).filter((message) => readCallMessage(message)?.event === "ended").map((message) => message.callId));
25875
25970
  for (const message of projectedMessages) {
25876
25971
  if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
25877
25972
  continue;
25878
- const call = readCallMessage(message);
25879
- if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined && !endedCalls.has(call.callId) || options.activeCallId === call.callId))
25880
- continue;
25881
- const readTarget = isPaymentRequest(message) ? message.paymentConfirmation : message;
25882
- if (!readTarget)
25883
- continue;
25973
+ const readTarget = isPaymentRequest(message) && message.paymentConfirmation ? message.paymentConfirmation : message;
25884
25974
  const originalSeenAt = allReadersSeenAt(message, members, evidence, options.memberStatesByEpoch);
25885
25975
  const resultSeenAt = readTarget === message ? originalSeenAt : allReadersSeenAt(readTarget, members, evidence, options.memberStatesByEpoch);
25886
25976
  if (originalSeenAt == null || resultSeenAt == null)
@@ -29858,6 +29948,20 @@ function uniqueValues(items) {
29858
29948
  function sortedUniqueValues(items) {
29859
29949
  return uniqueValues(items).sort();
29860
29950
  }
29951
+ function sameArray(a, b) {
29952
+ if (a === b) {
29953
+ return true;
29954
+ }
29955
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
29956
+ return false;
29957
+ }
29958
+ for (let index = 0;index < a.length; index += 1) {
29959
+ if (a[index] !== b[index]) {
29960
+ return false;
29961
+ }
29962
+ }
29963
+ return true;
29964
+ }
29861
29965
 
29862
29966
  // ../../core/chat/chats.js
29863
29967
  "use client";
@@ -31115,7 +31219,8 @@ function writeCachedChats(cache, chats, options = {}) {
31115
31219
  }
31116
31220
  }
31117
31221
  for (const item of items) {
31118
- byId.set(item.id, item);
31222
+ const previous = reviveChat(payload.chatsById?.[item.id]);
31223
+ byId.set(item.id, previous ? serializeChat(mergeChatActivity(item, previous, options.chatPK)) : item);
31119
31224
  }
31120
31225
  const merged = [...byId.values()];
31121
31226
  const capped = merged.length > LOCAL_CHAT_CACHE_MAX_ITEMS;
@@ -31273,18 +31378,24 @@ function readCachedReadFrontiers(cache, chatId) {
31273
31378
  return [];
31274
31379
  return Object.values(source).map(cleanFrontier2).filter(Boolean);
31275
31380
  }
31276
- function writeCachedReadFrontier(cache, chatId, value) {
31381
+ function writeCachedChatRead(cache, chatId, { frontier: value, message, openedAt, chatPK } = {}) {
31277
31382
  const frontier = cleanFrontier2(value);
31278
- if (!cache?.patch || !chatId || !frontier)
31383
+ if (!cache?.patch || !chatId || !frontier && !message)
31279
31384
  return Promise.resolve(false);
31280
31385
  return cache.patch((payload) => {
31281
- const chats = payload.pendingReadFrontiersByChat || {};
31282
- const current = chats[chatId] || {};
31283
- chats[chatId] = {
31284
- ...current,
31285
- [frontier.epochId]: laterFrontier(cleanFrontier2(current[frontier.epochId]), frontier)
31286
- };
31287
- payload.pendingReadFrontiersByChat = chats;
31386
+ if (frontier) {
31387
+ const chats = payload.pendingReadFrontiersByChat || {};
31388
+ const current = chats[chatId] || {};
31389
+ chats[chatId] = {
31390
+ ...current,
31391
+ [frontier.epochId]: laterFrontier(cleanFrontier2(current[frontier.epochId]), frontier)
31392
+ };
31393
+ payload.pendingReadFrontiersByChat = chats;
31394
+ }
31395
+ const chat = payload.chatsById?.[chatId];
31396
+ if (chat && message) {
31397
+ [payload.chatsById[chatId]] = markChatsRead([chat], chatId, message, { openedAt, chatPK });
31398
+ }
31288
31399
  return payload;
31289
31400
  }, { flush: true }).then(() => true);
31290
31401
  }
@@ -31640,6 +31751,15 @@ function createPendingSendQueue({ diag = null } = {}) {
31640
31751
  const generationRef = { current: 0 };
31641
31752
  const sentAtRef = { current: [] };
31642
31753
  const timerRef = { current: null };
31754
+ let active = true;
31755
+ let scheduleVersion = 0;
31756
+ function clearSchedule() {
31757
+ scheduleVersion += 1;
31758
+ if (timerRef.current !== null)
31759
+ clearTimeout(timerRef.current);
31760
+ timerRef.current = null;
31761
+ scheduledRef.current = false;
31762
+ }
31643
31763
  function nextRateDelay() {
31644
31764
  const now = Date.now();
31645
31765
  const since = now - CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS;
@@ -31681,6 +31801,8 @@ function createPendingSendQueue({ diag = null } = {}) {
31681
31801
  }
31682
31802
  }
31683
31803
  const flush = () => {
31804
+ if (!active)
31805
+ return;
31684
31806
  if (scheduledRef.current || !queueRef.current.length) {
31685
31807
  markDiag(diag, "chat.send.queue.flush.skip", {
31686
31808
  scheduled: !!scheduledRef.current,
@@ -31706,7 +31828,10 @@ function createPendingSendQueue({ diag = null } = {}) {
31706
31828
  running: runningJobsRef.current.size,
31707
31829
  sentWindow: sentAtRef.current.length
31708
31830
  });
31831
+ const version = ++scheduleVersion;
31709
31832
  timerRef.current = setTimeout(() => {
31833
+ if (!active || version !== scheduleVersion)
31834
+ return;
31710
31835
  timerRef.current = null;
31711
31836
  scheduledRef.current = false;
31712
31837
  const nextIndex = nextRunnableJobIndex();
@@ -31801,6 +31926,7 @@ function createPendingSendQueue({ diag = null } = {}) {
31801
31926
  };
31802
31927
  const resetPendingSendQueue = () => {
31803
31928
  generationRef.current += 1;
31929
+ clearSchedule();
31804
31930
  if (queueRef.current.length) {
31805
31931
  const error = new Error("chat reset");
31806
31932
  queueRef.current.forEach((job) => {
@@ -31809,16 +31935,18 @@ function createPendingSendQueue({ diag = null } = {}) {
31809
31935
  });
31810
31936
  }
31811
31937
  queueRef.current = [];
31812
- scheduledRef.current = false;
31813
31938
  sentAtRef.current = [];
31814
- if (timerRef.current) {
31815
- clearTimeout(timerRef.current);
31816
- timerRef.current = null;
31817
- }
31818
31939
  };
31819
31940
  return {
31820
31941
  enqueuePendingSendJob,
31821
31942
  resetPendingSendQueue,
31943
+ setActive(value) {
31944
+ active = value === true;
31945
+ clearSchedule();
31946
+ markDiag(diag, "chat.send.queue.activity", { active, queued: queueRef.current.length, running: runningJobsRef.current.size });
31947
+ if (active)
31948
+ flush();
31949
+ },
31822
31950
  close: resetPendingSendQueue
31823
31951
  };
31824
31952
  }
@@ -33454,6 +33582,7 @@ ${cid}` : "";
33454
33582
  };
33455
33583
  return {
33456
33584
  resetSending,
33585
+ setActive: pendingSendQueueRef.current.setActive,
33457
33586
  ackMessages,
33458
33587
  stageMessage: showLocalMessage,
33459
33588
  discardLocalMessage: (chatId, cid) => ackMessages(chatId, [cid], { remove: true }),
@@ -36097,7 +36226,7 @@ function createChatListCache({ getSources, state }) {
36097
36226
  const pending = pendingWrite;
36098
36227
  pendingWrite = null;
36099
36228
  if (pending?.cache && Array.isArray(pending.chats)) {
36100
- writeCachedChats(pending.cache, pending.chats, { ownerListToken: pending.ownerListToken });
36229
+ writeCachedChats(pending.cache, pending.chats, { ownerListToken: pending.ownerListToken, chatPK: pending.chatPK });
36101
36230
  }
36102
36231
  };
36103
36232
  const queue = (nextChats, options = {}) => {
@@ -36109,6 +36238,7 @@ function createChatListCache({ getSources, state }) {
36109
36238
  const requestedToken = requestedCoverage === CACHE_COVERAGE.COMPLETE ? cleanOwnerListToken(options.ownerListToken) : requestedCoverage === CACHE_COVERAGE.INCOMPLETE ? null : pendingWrite?.ownerListToken;
36110
36239
  pendingWrite = {
36111
36240
  cache: sources.localCache,
36241
+ chatPK: sources.chatPK,
36112
36242
  chats: nextChats,
36113
36243
  ownerListToken: requestedToken
36114
36244
  };
@@ -36135,6 +36265,11 @@ function createChatListCache({ getSources, state }) {
36135
36265
  state.setHasMoreChats(false);
36136
36266
  }
36137
36267
  hydrateReadCache(cachedChats, sources.readCacheRef.current);
36268
+ for (const chatId of Object.keys(cachedPayload?.pendingReadFrontiersByChat || {})) {
36269
+ const at = Math.max(0, ...readCachedReadFrontiers(sources.localCache, chatId).map((frontier) => frontier.at));
36270
+ if (at > (sources.readCacheRef.current.get(chatId) || 0))
36271
+ sources.readCacheRef.current.set(chatId, at);
36272
+ }
36138
36273
  const cachedChatsWithRead = applyReadCache(cachedChats, sources.chatPK, sources.readCacheRef.current);
36139
36274
  if (hasSavedChatSnapshot) {
36140
36275
  const shownCachedChats = hydrateRows(cachedChatsWithRead);
@@ -38907,6 +39042,14 @@ function createChatListEpochOwner({ getGeneration, getSources, index }) {
38907
39042
  }
38908
39043
 
38909
39044
  // ../../core/chat/listindex.js
39045
+ function sameRecord(left, right) {
39046
+ if (left === right)
39047
+ return true;
39048
+ if (!left || !right)
39049
+ return false;
39050
+ const keys = Object.keys(left);
39051
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && Object.is(left[key], right[key]));
39052
+ }
38910
39053
  function ownerRevision(chat) {
38911
39054
  return Number.isSafeInteger(chat?.ownEntry?.ownerRevision) ? chat.ownEntry.ownerRevision : 0;
38912
39055
  }
@@ -38936,11 +39079,26 @@ function activityFromChat(chat) {
38936
39079
  unseen: chat.unseen === true
38937
39080
  };
38938
39081
  }
38939
- function structuralChat(chat) {
39082
+ function structuralChat(chat, previous) {
38940
39083
  if (!chat)
38941
39084
  return null;
39085
+ const structure = {};
39086
+ if (previous && previous.epochId === chat.epochId && previous.epochVersion === chat.epochVersion) {
39087
+ for (const key of ["memberChatPKs", "memberUids"]) {
39088
+ if (sameArray(previous[key], chat[key]))
39089
+ structure[key] = previous[key];
39090
+ }
39091
+ for (const key of ["signingKeysByChatKey", "settings"]) {
39092
+ if (sameRecord(previous[key], chat[key]))
39093
+ structure[key] = previous[key];
39094
+ }
39095
+ 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]))) {
39096
+ structure.members = previous.members;
39097
+ }
39098
+ }
38942
39099
  return {
38943
39100
  ...chat,
39101
+ ...structure,
38944
39102
  preview: null,
38945
39103
  readMs: null,
38946
39104
  inboxMessageId: null,
@@ -39004,7 +39162,7 @@ function sourceEntry() {
39004
39162
  presentation: null
39005
39163
  };
39006
39164
  }
39007
- function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenChatId, state }) {
39165
+ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenChatId, state, onOwnersChanged }) {
39008
39166
  const entries = new Map;
39009
39167
  const authorityBoundaries = new Map;
39010
39168
  let authorityRevision = 0;
@@ -39125,8 +39283,14 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39125
39283
  const visible = sources.blockedReady ? filterBlockedChats(composedRows()) : [];
39126
39284
  const trimmed = trimExpiredChatPreviews(visible);
39127
39285
  const shown = sources.blockedReady ? sortedChats(filterBlockedChats(setLocalChats(trimmed, sources.localByChatRef.current))) : [];
39128
- sources.chatsRef.current = shown;
39129
- state.setChats((current) => sameChats(current, shown) ? current : shown);
39286
+ state.setChats((current) => {
39287
+ const byId = new Map(current.map((chat) => [chat.id, chat]));
39288
+ const next = shown.map((chat) => {
39289
+ const previous = byId.get(chat.id);
39290
+ return previous && sameChats([previous], [chat]) ? previous : chat;
39291
+ });
39292
+ return sameArray(current, next) ? current : next;
39293
+ });
39130
39294
  const last = getLastChat(shown);
39131
39295
  state.setLastChat((current) => sameLastChat(current, last) ? current : last);
39132
39296
  updatePeers(shown);
@@ -39144,6 +39308,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39144
39308
  if (options.warm !== false) {
39145
39309
  sources.warmChats(sources.blockedReady ? filterBlockedChats(owners) : []);
39146
39310
  }
39311
+ onOwnersChanged?.(owners);
39147
39312
  return owners;
39148
39313
  };
39149
39314
  const applyBlockedFilter = () => {
@@ -39169,7 +39334,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39169
39334
  for (const chat of filterHiddenChats(rows)) {
39170
39335
  const entry = entryFor(chat.id);
39171
39336
  captureActivity(entry, chat);
39172
- entry.cache = structuralChat(chat);
39337
+ entry.cache = structuralChat(chat, entry.cache);
39173
39338
  }
39174
39339
  const shown = render();
39175
39340
  updatePeers(shown);
@@ -39219,7 +39384,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39219
39384
  for (const chat of incoming) {
39220
39385
  const entry = entryFor(chat.id);
39221
39386
  captureActivity(entry, chat);
39222
- entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat));
39387
+ entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat, structuralOwnerFor(entry) || entry.cache));
39223
39388
  if (entry.committedOwner && compareOwners(entry.observedOwner, entry.committedOwner) >= 0) {
39224
39389
  entry.committedOwner = null;
39225
39390
  }
@@ -39237,7 +39402,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39237
39402
  for (const chat of (rows || []).filter((item) => item?.id && sourceCanPublish(item, options.sourceRevision))) {
39238
39403
  const entry = entryFor(chat.id);
39239
39404
  captureActivity(entry, chat);
39240
- entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat));
39405
+ entry.observedOwner = selectOwner(entry.observedOwner, structuralChat(chat, structuralOwnerFor(entry) || entry.cache));
39241
39406
  if (entry.committedOwner && compareOwners(entry.observedOwner, entry.committedOwner) >= 0) {
39242
39407
  entry.committedOwner = null;
39243
39408
  }
@@ -39257,7 +39422,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39257
39422
  return false;
39258
39423
  const entry = entryFor(chat.id);
39259
39424
  captureActivity(entry, chat);
39260
- entry.committedOwner = selectOwner(entry.committedOwner, structuralChat(chat));
39425
+ entry.committedOwner = selectOwner(entry.committedOwner, structuralChat(chat, structuralOwnerFor(entry) || entry.cache));
39261
39426
  if (ownerCoversAnnouncement(ownerFor(entry), entry.announcement))
39262
39427
  entry.announcement = null;
39263
39428
  entry.cache = null;
@@ -39433,6 +39598,9 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
39433
39598
  });
39434
39599
  if (next === current)
39435
39600
  return false;
39601
+ const readCache = getSources().readCacheRef.current;
39602
+ if (next.readMs > (readCache.get(chatId) || 0))
39603
+ readCache.set(chatId, next.readMs);
39436
39604
  replaceActivity(entry, next);
39437
39605
  publishOwners();
39438
39606
  return true;
@@ -40085,6 +40253,75 @@ function createChatListPreview({ index, isListening, state }) {
40085
40253
  };
40086
40254
  }
40087
40255
 
40256
+ // ../../core/chat/listreads.js
40257
+ function createChatListReads({ getSources, index }) {
40258
+ const attempted = new Set;
40259
+ let queue = [];
40260
+ let generation = 0;
40261
+ let running = false;
40262
+ const admitted = (chat, sources) => !sources.chatBanned && !(chat?.memberUids || []).some((uid) => sources.blockedUidSet.has(uid)) && chat?.unseen && chat.ownEntry && !isChatMembershipRemoved(chat);
40263
+ const eligible = (chat, sources) => sources.isActive && admitted(chat, sources);
40264
+ const pump = async () => {
40265
+ if (running)
40266
+ return;
40267
+ running = true;
40268
+ const run = generation;
40269
+ try {
40270
+ while (queue.length && run === generation) {
40271
+ const { chatId, epochId, key } = queue.shift();
40272
+ const sources = getSources();
40273
+ const chat = index.getOwnerChat(chatId);
40274
+ if (!eligible(chat, sources)) {
40275
+ attempted.delete(key);
40276
+ continue;
40277
+ }
40278
+ try {
40279
+ const epoch = epochId === chat.epochId ? chat.epochState : await sources.getEpochState(chat, epochId);
40280
+ if (run !== generation)
40281
+ return;
40282
+ const state = epoch ? await readChatMemberState(sources.cloud, epoch, sources.chatPK) : null;
40283
+ if (run !== generation || !admitted(index.getOwnerChat(chatId), getSources()))
40284
+ continue;
40285
+ const frontier = state?.readFrontier;
40286
+ if (frontier)
40287
+ index.markChatRead(chatId, { id: frontier.id, ts: frontier.at }, frontier.seenAt);
40288
+ } catch (error) {
40289
+ if (run !== generation)
40290
+ return;
40291
+ markError(sources.diag, "chat.list.read.restore", Date.now(), error);
40292
+ }
40293
+ }
40294
+ } finally {
40295
+ if (run === generation)
40296
+ running = false;
40297
+ }
40298
+ };
40299
+ return {
40300
+ restore(chats) {
40301
+ const sources = getSources();
40302
+ if (typeof sources.cloud?.chat?.memberState?.readSlot !== "function")
40303
+ return;
40304
+ for (const chat of chats) {
40305
+ if (!eligible(chat, sources))
40306
+ continue;
40307
+ const epochId = chat.preview?.epochId || chat.epochId;
40308
+ const key = `${chat.id}:${epochId}`;
40309
+ if (attempted.has(key))
40310
+ continue;
40311
+ attempted.add(key);
40312
+ queue.push({ chatId: chat.id, epochId, key });
40313
+ }
40314
+ return pump();
40315
+ },
40316
+ reset() {
40317
+ generation += 1;
40318
+ attempted.clear();
40319
+ queue = [];
40320
+ running = false;
40321
+ }
40322
+ };
40323
+ }
40324
+
40088
40325
  // ../../core/chat/liststate.js
40089
40326
  function nextValue(current, next) {
40090
40327
  return typeof next === "function" ? next(current) : next;
@@ -40263,7 +40500,16 @@ function createChatList(initialSources) {
40263
40500
  const filterHiddenChats = (chats) => (chats || []).filter((chatItem) => chatItem?.id && !isHiddenChatId(chatItem.id));
40264
40501
  const state = createChatListState({ getSources });
40265
40502
  const cache = createChatListCache({ getSources, state });
40266
- const index = createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenChatId, state });
40503
+ let reads = null;
40504
+ const index = createChatListIndex({
40505
+ cache,
40506
+ filterHiddenChats,
40507
+ getSources,
40508
+ isHiddenChatId,
40509
+ state,
40510
+ onOwnersChanged: (chats) => reads?.restore(chats)
40511
+ });
40512
+ reads = createChatListReads({ getSources, index });
40267
40513
  const epoch = createChatListEpochOwner({ getGeneration, getSources, index });
40268
40514
  const paging = createChatListPaging({ cache, getGeneration, getSources, index, isHiddenChatId, state });
40269
40515
  const invalidateRuntimeTasks = () => {
@@ -40272,6 +40518,7 @@ function createChatList(initialSources) {
40272
40518
  epoch.reset();
40273
40519
  };
40274
40520
  const resetChatList = (ready = false) => {
40521
+ reads.reset();
40275
40522
  invalidateRuntimeTasks();
40276
40523
  index.reset(ready);
40277
40524
  };
@@ -40322,6 +40569,7 @@ function createChatList(initialSources) {
40322
40569
  index.applyBlockedFilter();
40323
40570
  paging.fillVisibleChatPage();
40324
40571
  }
40572
+ reads.restore(index.getOwnerChats());
40325
40573
  };
40326
40574
  const start2 = () => {
40327
40575
  if (listener.isListening())
@@ -40330,6 +40578,7 @@ function createChatList(initialSources) {
40330
40578
  preview.schedule();
40331
40579
  };
40332
40580
  const close = () => {
40581
+ reads.reset();
40333
40582
  listener.close();
40334
40583
  preview.close();
40335
40584
  cache.flush();
@@ -42456,9 +42705,6 @@ function createChatSessionPending({
42456
42705
  return track(launchId, previousOperation.then((result) => sendDurableSharedAttachment(result?.chatId || launchId, queuedAttachment, options)));
42457
42706
  };
42458
42707
  const sendChatMessage = (chatId, message, options = {}) => {
42459
- const chat = getOwnerChat(resolvePendingChatId(chatId)) || launchForChat(chatId)?.chat;
42460
- if (message?.tokenIdentifier != null && chat?.lineage === "group")
42461
- throw new Error("group payments support bitcoin only");
42462
42708
  const operation = operationForChat(chatId);
42463
42709
  if (operation?.discarding)
42464
42710
  throw makeChatUnavailableError();
@@ -43862,7 +44108,6 @@ function createChatSeen({
43862
44108
  const scheduleRead = (chatId, message, previewMs, frontierMessage, messages = []) => {
43863
44109
  const frontier = Number.isSafeInteger(frontierMessage?.at) && frontierMessage?.epochId ? frontierMessage : readFrontierFromMessage(frontierMessage, { seenAt: previewMs });
43864
44110
  if (frontier) {
43865
- writeCachedReadFrontier(localCache, chatId, frontier);
43866
44111
  onReadFrontier?.(chatId, frontier);
43867
44112
  }
43868
44113
  const timing = getReadWriteTiming?.(chatId) || {};
@@ -43896,6 +44141,12 @@ function createChatSeen({
43896
44141
  const frontier = readFrontierFromMessage(frontierMessage, { seenAt: read?.previewMs || Date.now() });
43897
44142
  if (!read && !frontier && !cachedFrontiers.length)
43898
44143
  return false;
44144
+ const savedRead = writeCachedChatRead(localCache, chatId, {
44145
+ frontier,
44146
+ message: read?.preview,
44147
+ openedAt: read?.previewMs,
44148
+ chatPK
44149
+ }).catch((error) => markError(diag, "chat.read.cache", Date.now(), error));
43899
44150
  if (read) {
43900
44151
  markChatRead?.(chatId, read.preview, read.previewMs);
43901
44152
  readCacheRef.current.set(chatId, read.readMs);
@@ -43905,6 +44156,7 @@ function createChatSeen({
43905
44156
  for (const cached of cachedFrontiers)
43906
44157
  scheduleRead(chatId, preview, previewMs, cached, messages);
43907
44158
  scheduleRead(chatId, preview, previewMs, frontierMessage, messages);
44159
+ await savedRead;
43908
44160
  return true;
43909
44161
  };
43910
44162
  const markChatReadState = async (chatId, message, options = {}) => {
@@ -43992,6 +44244,7 @@ function createChatSessionActions({
43992
44244
  diag,
43993
44245
  readWriteInterval,
43994
44246
  adoptLocalMessageMedia,
44247
+ getActive,
43995
44248
  getIdentity,
43996
44249
  getLiveActivity,
43997
44250
  getOwnerChat,
@@ -44123,6 +44376,7 @@ function createChatSessionActions({
44123
44376
  reconcileEpoch: (chatId) => getChatListOwner().reconcileChatEpoch(chatId),
44124
44377
  diag
44125
44378
  });
44379
+ sendActions.setActive(getActive());
44126
44380
  reactionActions = createChatReaction({
44127
44381
  cloud,
44128
44382
  uid: identity().uid,
@@ -44267,10 +44521,10 @@ function sameReadFrontier(left, right) {
44267
44521
  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;
44268
44522
  }
44269
44523
  function sameActivity2(left, right) {
44270
- 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]));
44524
+ 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]));
44271
44525
  }
44272
44526
  function hasActivity(activity) {
44273
- return !!(activity?.connected || activity?.activeChatPKs?.length || activity?.compositions?.length || activity?.readFrontiers?.length);
44527
+ return !!(activity?.connected || activity?.activeChatPKs?.length || activity?.compositions?.length || activity?.readFrontiers?.length || Object.keys(activity?.lastObservedAtByChatPK || {}).length);
44274
44528
  }
44275
44529
  function summarizeLocalMessage(summary, message) {
44276
44530
  summary.messages += 1;
@@ -44411,6 +44665,17 @@ function createChatSessionState({
44411
44665
  return;
44412
44666
  const next = new Map(liveActivityByChat);
44413
44667
  const previous = next.get(chatId);
44668
+ let lastObservedAtByChatPK = previous?.lastObservedAtByChatPK;
44669
+ const departed = previous?.connected ? previous.activeChatPKs.filter((actor) => !activity?.connected || !activity.activeChatPKs.includes(actor)) : [];
44670
+ if (departed.length) {
44671
+ const now = Date.now();
44672
+ lastObservedAtByChatPK = { ...lastObservedAtByChatPK };
44673
+ for (const actor of departed)
44674
+ lastObservedAtByChatPK[actor] = Math.max(lastObservedAtByChatPK[actor] || 0, now);
44675
+ Object.freeze(lastObservedAtByChatPK);
44676
+ }
44677
+ if (lastObservedAtByChatPK)
44678
+ activity = { ...activity, lastObservedAtByChatPK };
44414
44679
  if (previous?.connected && !activity?.connected)
44415
44680
  onLiveDisconnect(chatId);
44416
44681
  if (hasActivity(activity)) {
@@ -44805,6 +45070,7 @@ function normalizeChatSessionSources(sources = {}) {
44805
45070
  uid: sources.uid || "",
44806
45071
  online: sources.online !== false,
44807
45072
  blocked: Array.isArray(sources.blocked) ? sources.blocked : [],
45073
+ blockedPending: Array.isArray(sources.blockedPending) ? sources.blockedPending : [],
44808
45074
  blockedReady: sources.blockedReady === true,
44809
45075
  chatPK: sources.chatPK || "",
44810
45076
  chatBanned: sources.chatBanned === true,
@@ -44853,7 +45119,6 @@ function createChatSession({
44853
45119
  mls = null,
44854
45120
  live = null,
44855
45121
  maintenance,
44856
- resolveActiveCall,
44857
45122
  incomingChatDecision = null,
44858
45123
  getPeerProfile = null,
44859
45124
  refreshPeerProfile = null,
@@ -44871,6 +45136,7 @@ function createChatSession({
44871
45136
  uid = "",
44872
45137
  online = true,
44873
45138
  blocked = [],
45139
+ blockedPending = [],
44874
45140
  blockedReady = false,
44875
45141
  chatPK = "",
44876
45142
  chatSigningPK = "",
@@ -44887,6 +45153,7 @@ function createChatSession({
44887
45153
  let inboxEnabled = initialSources.inboxEnabled === true;
44888
45154
  let started = false;
44889
45155
  let appStateSubscription = null;
45156
+ const messageListeners = new Set;
44890
45157
  let mlsPoolReplenishment = null;
44891
45158
  let mlsPoolReplenishQueued = false;
44892
45159
  let mlsPoolNeedsReplenish = true;
@@ -45061,6 +45328,13 @@ function createChatSession({
45061
45328
  };
45062
45329
  const handleBatchMessages = (chatId, messages) => {
45063
45330
  chatListOwner.observeMessages(chatId, messages);
45331
+ for (const listener of messageListeners) {
45332
+ try {
45333
+ listener(chatId, messages);
45334
+ } catch (error) {
45335
+ diag?.("chat.message.observer.error", { code: error?.code || "chat/observer" });
45336
+ }
45337
+ }
45064
45338
  if (![...messages || []].reverse().some(isEpochTransitionMsg))
45065
45339
  return;
45066
45340
  const startedAt = Date.now();
@@ -45493,8 +45767,6 @@ function createChatSession({
45493
45767
  const chat = getOwnerChat(chatId);
45494
45768
  if (!chat)
45495
45769
  throw makeChatUnavailableError();
45496
- if (payment.tokenIdentifier != null && chat.lineage !== "direct")
45497
- throw new Error("group payments support bitcoin only");
45498
45770
  const members = Array.isArray(chat?.memberChatPKs) ? chat.memberChatPKs.map((memberChatPK) => cleanText(memberChatPK).toLowerCase()) : (chat?.members || []).map((member) => cleanText(member?.chatPK).toLowerCase());
45499
45771
  const message = pending ? makePendingChatPayment(payment) : makeChatPayment(payment);
45500
45772
  const recipients = chatPaymentRecipients(message);
@@ -45521,6 +45793,7 @@ function createChatSession({
45521
45793
  };
45522
45794
  const getLocalMessages = (chatId) => isChatPendingDelete(chatId) || !isChatIdVisible(chatId) ? EMPTY_LOCAL_MESSAGES : localByChatRef.current.get(chatId) ?? EMPTY_LOCAL_MESSAGES;
45523
45795
  const blockedAuthority = (values) => [...values || []].sort().join("|");
45796
+ const committedBlocked = () => blocked.filter((uid2) => !blockedPending.includes(uid2));
45524
45797
  const bulkOperation = (blockedUids = null) => ({
45525
45798
  generation: authGeneration,
45526
45799
  identity: {
@@ -45536,7 +45809,7 @@ function createChatSession({
45536
45809
  deleteActions: actionOwner.deleteActions,
45537
45810
  blockedKey: blockedUids ? blockedAuthority(blockedUids) : null
45538
45811
  });
45539
- const isBulkOperationCurrent = (operation) => operation.generation === authGeneration && operation.deleteActions === actionOwner.deleteActions && (operation.blockedKey == null || operation.blockedKey === blockedAuthority(blocked));
45812
+ const isBulkOperationCurrent = (operation) => operation.generation === authGeneration && operation.deleteActions === actionOwner.deleteActions && (operation.blockedKey == null || operation.blockedKey === blockedAuthority(committedBlocked()));
45540
45813
  const assertBulkOperationCurrent = (operation) => {
45541
45814
  requireOnline();
45542
45815
  if (!isBulkOperationCurrent(operation))
@@ -45550,32 +45823,42 @@ function createChatSession({
45550
45823
  return 0;
45551
45824
  let allChats = await loadOwnerChats(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey);
45552
45825
  assertBulkOperationCurrent(operation);
45553
- let processedInbox = true;
45554
- while (processedInbox) {
45555
- processedInbox = await processInbox(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey, {
45556
- currentChats: allChats,
45557
- chatSigningPK: identity.chatSigningPK,
45558
- chatSigningSecret: identity.chatSigningSecret,
45559
- notificationPK: identity.notificationPK,
45560
- notificationPrivateKey: identity.notificationPrivateKey,
45561
- mls,
45562
- blockedUids: new Set(identity.blocked),
45563
- chatAdmission,
45564
- incomingChatDecision,
45565
- scanAllSlots: true,
45566
- requireComplete: true
45567
- });
45568
- assertBulkOperationCurrent(operation);
45569
- if (processedInbox) {
45570
- allChats = await loadOwnerChats(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey);
45571
- assertBulkOperationCurrent(operation);
45572
- }
45573
- }
45826
+ await processInbox(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey, {
45827
+ currentChats: allChats,
45828
+ chatSigningPK: identity.chatSigningPK,
45829
+ chatSigningSecret: identity.chatSigningSecret,
45830
+ notificationPK: identity.notificationPK,
45831
+ notificationPrivateKey: identity.notificationPrivateKey,
45832
+ mls,
45833
+ blockedUids: new Set(identity.blocked),
45834
+ chatAdmission,
45835
+ incomingChatDecision,
45836
+ scanAllSlots: true,
45837
+ requireComplete: true,
45838
+ isCurrent: () => isBulkOperationCurrent(operation),
45839
+ diag
45840
+ });
45841
+ assertBulkOperationCurrent(operation);
45842
+ allChats = await loadOwnerChats(cloud, identity.uid, identity.chatPK, identity.chatPrivateKey);
45843
+ assertBulkOperationCurrent(operation);
45574
45844
  if (!allChats.length)
45575
45845
  return 0;
45576
45846
  let retired = 0;
45577
- for (const chat of allChats) {
45847
+ for (const ownedChat of allChats) {
45578
45848
  assertBulkOperationCurrent(operation);
45849
+ let chat = ownedChat;
45850
+ if (!isChatMembershipRemoved(chat)) {
45851
+ chatListOwner.commitOwner(chat);
45852
+ const reconciliation = await chatListOwner.reconcileChatEpoch(chat.id);
45853
+ assertBulkOperationCurrent(operation);
45854
+ chat = getOwnerChat(chat.id);
45855
+ if (!chat) {
45856
+ if (!reconciliation.removed)
45857
+ throw makeChatUnavailableError();
45858
+ retired += 1;
45859
+ continue;
45860
+ }
45861
+ }
45579
45862
  if (isChatMembershipRemoved(chat)) {
45580
45863
  await operation.deleteActions.deleteChat(chat, { cleanup: false });
45581
45864
  assertBulkOperationCurrent(operation);
@@ -45717,9 +46000,10 @@ function createChatSession({
45717
46000
  return false;
45718
46001
  }
45719
46002
  };
45720
- const prepareCall = async (chatId) => {
46003
+ const prepareCall = async (chatId, options = {}) => {
45721
46004
  const generation = authGeneration;
45722
- const current = () => generation === authGeneration && online && pendingOwner.canSendToChat(chatId);
46005
+ await sendReadiness.wait(chatId, options);
46006
+ const current = () => generation === authGeneration && !options.signal?.aborted && online && pendingOwner.canSendToChat(chatId);
45723
46007
  if (!current())
45724
46008
  throw new Error("chat unavailable");
45725
46009
  await pendingOwner.assertChatAdmission(chatId);
@@ -45736,7 +46020,13 @@ function createChatSession({
45736
46020
  throw new Error("chat unavailable");
45737
46021
  return chat.epochState;
45738
46022
  };
45739
- const materializeChat = (chatId) => pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
46023
+ const materializeChat = async (chatId, options = {}) => {
46024
+ const generation = authGeneration;
46025
+ await sendReadiness.wait(chatId, options);
46026
+ if (generation !== authGeneration || options.signal?.aborted)
46027
+ throw makeChatUnavailableError();
46028
+ return pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
46029
+ };
45740
46030
  const flushChatReadFrontier = (chatId) => {
45741
46031
  if (!online)
45742
46032
  return false;
@@ -45928,6 +46218,7 @@ function createChatSession({
45928
46218
  lastHydratedCacheKeyRef,
45929
46219
  chatsRef,
45930
46220
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
46221
+ getEpochState: (...args) => historyOwner.getEpochState(...args),
45931
46222
  warmChats: (...args) => messageBatchOwner.warm(...args),
45932
46223
  onOwnerRemoved: (chatIds) => {
45933
46224
  for (const chatId of chatIds)
@@ -45986,6 +46277,7 @@ function createChatSession({
45986
46277
  });
45987
46278
  };
45988
46279
  stateOwner.setActions({
46280
+ ensureChat,
45989
46281
  loadMoreChats: loadMoreChats2,
45990
46282
  selectChat,
45991
46283
  resolveChatRequest,
@@ -46054,7 +46346,6 @@ function createChatSession({
46054
46346
  deleteMessage,
46055
46347
  deleteMessages,
46056
46348
  deleteMessageDocs,
46057
- resolveActiveCall,
46058
46349
  setChatTtl,
46059
46350
  makeMessagePermanent,
46060
46351
  makeMessageTemporary,
@@ -46071,6 +46362,7 @@ function createChatSession({
46071
46362
  diag,
46072
46363
  readWriteInterval,
46073
46364
  adoptLocalMessageMedia,
46365
+ getActive: () => isActive,
46074
46366
  getIdentity: () => ({
46075
46367
  uid,
46076
46368
  chatPK,
@@ -46120,6 +46412,7 @@ function createChatSession({
46120
46412
  const setActive = (nextActive) => {
46121
46413
  foreground = nextActive === true;
46122
46414
  const value = foreground && online;
46415
+ actionOwner.sendActions.setActive(value);
46123
46416
  if (isActive === value)
46124
46417
  return;
46125
46418
  if (!value) {
@@ -46147,7 +46440,7 @@ function createChatSession({
46147
46440
  inboxTransport.setSource(inboxSource());
46148
46441
  };
46149
46442
  const setSources = (nextSources = {}) => {
46150
- const previousBlockedKey = [...blocked].sort().join("|");
46443
+ const previousBlockedKey = blockedAuthority(committedBlocked());
46151
46444
  const current = { uid, chatPK, chatBanned, chatPrivateKey, chatSigningPK, chatSigningSecret, notificationPK, notificationPrivateKey, chatAdmission, localCache };
46152
46445
  const next = normalizeChatSessionSources(nextSources);
46153
46446
  const admissionChanged = !sameChatAdmission(chatAdmission, next.chatAdmission);
@@ -46164,6 +46457,7 @@ function createChatSession({
46164
46457
  uid,
46165
46458
  online,
46166
46459
  blocked,
46460
+ blockedPending,
46167
46461
  blockedReady,
46168
46462
  chatPK,
46169
46463
  chatBanned,
@@ -46198,9 +46492,9 @@ function createChatSession({
46198
46492
  drainMembershipOutbox();
46199
46493
  replenishMlsPool({ force: true });
46200
46494
  }
46201
- const nextBlockedKey = [...blocked].sort().join("|");
46495
+ const nextBlockedKey = blockedAuthority(committedBlocked());
46202
46496
  if (online && blockedReady && nextBlockedKey && nextBlockedKey !== previousBlockedKey) {
46203
- retireBlockedChats(new Set(blocked)).catch((error) => {
46497
+ retireBlockedChats(new Set(committedBlocked())).catch((error) => {
46204
46498
  markDiag(diag, "chat.block.retire.failed", { message: error?.message || String(error) });
46205
46499
  });
46206
46500
  }
@@ -46220,8 +46514,8 @@ function createChatSession({
46220
46514
  publish();
46221
46515
  inboxTransport.start(inboxSource());
46222
46516
  chatListOwner.start();
46223
- if (blockedReady && blocked.length) {
46224
- retireBlockedChats(new Set(blocked)).catch((error) => {
46517
+ if (blockedReady && committedBlocked().length) {
46518
+ retireBlockedChats(new Set(committedBlocked())).catch((error) => {
46225
46519
  markDiag(diag, "chat.block.retire.failed", { message: error?.message || String(error) });
46226
46520
  });
46227
46521
  }
@@ -46234,6 +46528,7 @@ function createChatSession({
46234
46528
  };
46235
46529
  const close = () => {
46236
46530
  authGeneration += 1;
46531
+ messageListeners.clear();
46237
46532
  sendReadiness.reset();
46238
46533
  pendingOwner.reset();
46239
46534
  maintenance.close();
@@ -46259,7 +46554,20 @@ function createChatSession({
46259
46554
  const subscribeFields = stateOwner.subscribeFields;
46260
46555
  const getSnapshot = stateOwner.getSnapshot;
46261
46556
  publish();
46262
- return { close, getSnapshot, refreshAdmissionProjection, setInboxEnabled, setSources, start: start2, subscribe, subscribeFields };
46557
+ return {
46558
+ close,
46559
+ getSnapshot,
46560
+ refreshAdmissionProjection,
46561
+ setInboxEnabled,
46562
+ setSources,
46563
+ start: start2,
46564
+ subscribe,
46565
+ subscribeFields,
46566
+ subscribeMessages(listener) {
46567
+ messageListeners.add(listener);
46568
+ return () => messageListeners.delete(listener);
46569
+ }
46570
+ };
46263
46571
  }
46264
46572
 
46265
46573
  // ../../core/presence/protocol.js
@@ -46433,6 +46741,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46433
46741
  const peerListeners = new Set;
46434
46742
  const records = new Map;
46435
46743
  const peers = new Map;
46744
+ const peersByUid = new Map;
46436
46745
  const readyShards = new Map;
46437
46746
  let sources = { uid: null, identity: null, enabled: false };
46438
46747
  let channel = null;
@@ -46444,11 +46753,13 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46444
46753
  let seq = 0;
46445
46754
  let renewTimer = null;
46446
46755
  let expiryTimer = null;
46756
+ let expiryAt = Infinity;
46757
+ let ownTopic = null;
46447
46758
  let command = null;
46448
46759
  let authenticating = null;
46449
46760
  let snapshot = Object.freeze({ visibility: null, ready: false, pending: false, pendingVisibility: null, error: null, ...UNKNOWN_PRESENCE });
46450
46761
  function topic() {
46451
- return sources.uid ? derivePresenceTopic(realm2, sources.uid) : null;
46762
+ return ownTopic;
46452
46763
  }
46453
46764
  function isReady() {
46454
46765
  return Boolean(channel && challenge && policy && sources.enabled && !closed);
@@ -46481,17 +46792,19 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46481
46792
  const latest = fresh.reduce((selected, lease) => !selected || lease.issuedAt > selected.issuedAt || lease.issuedAt === selected.issuedAt && lease.session > selected.session ? lease : selected, null);
46482
46793
  return { availability: fresh.length ? "online" : "offline", status: latest?.status ?? null, lastActiveAt };
46483
46794
  }
46484
- function refresh() {
46485
- clearTimeout(expiryTimer);
46486
- expiryTimer = null;
46487
- let changed = false;
46795
+ function refresh(keys = records.keys(), changedUids = new Set) {
46488
46796
  let deadline = Infinity;
46489
46797
  const timestamp = now();
46490
- for (const record of records.values()) {
46798
+ for (const key of keys) {
46799
+ const record = records.get(key);
46800
+ if (!record)
46801
+ continue;
46491
46802
  const next = projection(record);
46492
46803
  if (!samePresence(record.view, next)) {
46493
46804
  record.view = Object.freeze(next);
46494
- changed = true;
46805
+ const uid = peers.get(key)?.uid;
46806
+ if (uid)
46807
+ changedUids.add(uid);
46495
46808
  }
46496
46809
  if (record.ready) {
46497
46810
  for (const lease of record.leases)
@@ -46504,11 +46817,18 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46504
46817
  }
46505
46818
  const own = records.get(topic())?.view || UNKNOWN_PRESENCE;
46506
46819
  publish(policy?.visibility === "private" ? OFFLINE_PRESENCE : own);
46507
- if (changed)
46820
+ if (changedUids.size)
46508
46821
  for (const listener of peerListeners)
46509
- listener();
46510
- if (Number.isFinite(deadline) && deadline > timestamp)
46511
- expiryTimer = setTimeout(refresh, deadline - timestamp + 1);
46822
+ listener(changedUids);
46823
+ if (deadline < expiryAt && deadline > timestamp) {
46824
+ clearTimeout(expiryTimer);
46825
+ expiryAt = deadline;
46826
+ expiryTimer = setTimeout(() => {
46827
+ expiryAt = Infinity;
46828
+ expiryTimer = null;
46829
+ refresh();
46830
+ }, deadline - timestamp + 1);
46831
+ }
46512
46832
  }
46513
46833
  function finishCommand(error = null) {
46514
46834
  if (!command)
@@ -46523,7 +46843,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46523
46843
  pending.resolve(snapshot);
46524
46844
  }
46525
46845
  function expected(identity, extra = {}) {
46526
- return { realm: realm2, topic: derivePresenceTopic(realm2, identity.uid), signingPK: identity.chatSigningPK, now: now(), ...extra };
46846
+ return { realm: realm2, topic: identity.topic, signingPK: identity.chatSigningPK, now: now(), ...extra };
46527
46847
  }
46528
46848
  function receive(frame) {
46529
46849
  const identity = peers.get(frame.topic);
@@ -46548,6 +46868,15 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46548
46868
  lastActiveAt = verifyPresenceObservation(frame.lastObservation, scope).lastActiveAt;
46549
46869
  } catch {}
46550
46870
  }
46871
+ const timestamp = now();
46872
+ if (previous?.revision === currentPolicy.revision) {
46873
+ lastActiveAt = Math.max(lastActiveAt || 0, previous.lastActiveAt || 0) || null;
46874
+ if (!frame.leases?.length && previous.ready && previous.leases.some((lease) => lease.expiresAt > timestamp)) {
46875
+ lastActiveAt = Math.max(lastActiveAt || 0, timestamp);
46876
+ }
46877
+ }
46878
+ for (const lease of leases)
46879
+ lastActiveAt = Math.max(lastActiveAt || 0, Math.min(lease.issuedAt, timestamp));
46551
46880
  }
46552
46881
  records.set(frame.topic, {
46553
46882
  ready: true,
@@ -46556,7 +46885,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46556
46885
  revision: currentPolicy?.revision ?? previous?.revision ?? 0,
46557
46886
  view: previous?.view || UNKNOWN_PRESENCE
46558
46887
  });
46559
- refresh();
46888
+ refresh([frame.topic]);
46560
46889
  } catch (error) {
46561
46890
  report(error);
46562
46891
  }
@@ -46574,10 +46903,10 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46574
46903
  if (!session || !policy || policy.visibility !== "public" || command)
46575
46904
  return;
46576
46905
  const timestamp = now();
46577
- const ownTopic = topic();
46906
+ const ownTopic2 = topic();
46578
46907
  const lease = signPresenceLease(sources.identity, {
46579
46908
  realm: realm2,
46580
- topic: ownTopic,
46909
+ topic: ownTopic2,
46581
46910
  session,
46582
46911
  challenge,
46583
46912
  policyRevision: policy.revision,
@@ -46588,11 +46917,11 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46588
46917
  });
46589
46918
  const observation = signPresenceObservation(sources.identity, {
46590
46919
  realm: realm2,
46591
- topic: ownTopic,
46920
+ topic: ownTopic2,
46592
46921
  policyRevision: policy.revision,
46593
46922
  lastActiveAt: Math.floor(timestamp / PRESENCE_LAST_ACTIVE_BUCKET_MS) * PRESENCE_LAST_ACTIVE_BUCKET_MS
46594
46923
  });
46595
- channel.send(presenceShard(ownTopic), { type: "lease", lease, observation });
46924
+ channel.send(presenceShard(ownTopic2), { type: "lease", lease, observation });
46596
46925
  }
46597
46926
  function receiveOwner(frame) {
46598
46927
  if (frame.topic !== topic() || !sources.identity)
@@ -46614,7 +46943,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46614
46943
  if (policy.visibility === "private") {
46615
46944
  const previous = records.get(topic());
46616
46945
  records.set(topic(), { ready: true, leases: [], lastActiveAt: null, revision: policy.revision, view: previous?.view || UNKNOWN_PRESENCE });
46617
- refresh();
46946
+ refresh([topic()]);
46618
46947
  }
46619
46948
  if (command && policy.revision >= command.policy.revision) {
46620
46949
  finishCommand(policy.signature === command.policy.signature ? null : new Error("activity setting changed on another device; try again"));
@@ -46725,7 +47054,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46725
47054
  function setPeers(profiles) {
46726
47055
  const next = new Map;
46727
47056
  if (sources.uid && sources.identity)
46728
- next.set(topic(), { uid: sources.uid, chatSigningPK: sources.identity.publicKey });
47057
+ next.set(topic(), { uid: sources.uid, topic: topic(), chatSigningPK: sources.identity.publicKey });
46729
47058
  for (const profile of profiles || []) {
46730
47059
  if (next.size >= PRESENCE_MAX_SUBSCRIPTIONS)
46731
47060
  break;
@@ -46733,27 +47062,34 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46733
47062
  continue;
46734
47063
  if (profile.uid === sources.uid)
46735
47064
  continue;
46736
- next.set(derivePresenceTopic(realm2, profile.uid), { uid: profile.uid, chatSigningPK: profile.chatSigningPK });
47065
+ const key = peersByUid.get(profile.uid)?.topic || derivePresenceTopic(realm2, profile.uid);
47066
+ next.set(key, { uid: profile.uid, topic: key, chatSigningPK: profile.chatSigningPK });
46737
47067
  }
46738
47068
  let changed = peers.size !== next.size;
47069
+ const changedUids = new Set;
46739
47070
  for (const [key, identity] of next) {
46740
47071
  if (peers.get(key)?.chatSigningPK !== identity.chatSigningPK) {
46741
47072
  changed = true;
47073
+ changedUids.add(identity.uid);
46742
47074
  records.delete(key);
46743
47075
  }
46744
47076
  }
47077
+ for (const [key, identity] of peers)
47078
+ if (!next.has(key))
47079
+ changedUids.add(identity.uid);
46745
47080
  if (!changed)
46746
47081
  return;
46747
47082
  peers.clear();
46748
- for (const [key, value] of next)
47083
+ peersByUid.clear();
47084
+ for (const [key, value] of next) {
46749
47085
  peers.set(key, value);
47086
+ peersByUid.set(value.uid, value);
47087
+ }
46750
47088
  for (const key of records.keys())
46751
47089
  if (!peers.has(key))
46752
47090
  records.delete(key);
46753
47091
  sync();
46754
- refresh();
46755
- for (const listener of peerListeners)
46756
- listener();
47092
+ refresh([], changedUids);
46757
47093
  }
46758
47094
  function update(next) {
46759
47095
  const accountChanged = sources.uid !== next.uid;
@@ -46762,11 +47098,15 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46762
47098
  stop();
46763
47099
  policy = null;
46764
47100
  records.clear();
46765
- if (accountChanged)
47101
+ if (accountChanged) {
46766
47102
  peers.clear();
47103
+ peersByUid.clear();
47104
+ }
46767
47105
  publish({ visibility: null, error: null, ...UNKNOWN_PRESENCE });
46768
47106
  }
46769
47107
  sources = next;
47108
+ if (accountChanged)
47109
+ ownTopic = next.uid ? derivePresenceTopic(realm2, next.uid) : null;
46770
47110
  setPeers([...peers.values()]);
46771
47111
  sync();
46772
47112
  }
@@ -46807,10 +47147,10 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46807
47147
  getPeer(profile) {
46808
47148
  if (!profile?.uid)
46809
47149
  return UNKNOWN_PRESENCE;
46810
- const key = derivePresenceTopic(realm2, profile.uid);
46811
- if (peers.get(key)?.chatSigningPK !== profile.chatSigningPK)
47150
+ const identity = peersByUid.get(profile.uid);
47151
+ if (!identity || identity.chatSigningPK !== profile.chatSigningPK)
46812
47152
  return UNKNOWN_PRESENCE;
46813
- return records.get(key)?.view || UNKNOWN_PRESENCE;
47153
+ return records.get(identity.topic)?.view || UNKNOWN_PRESENCE;
46814
47154
  },
46815
47155
  subscribe(listener) {
46816
47156
  listeners.add(listener);
@@ -46829,6 +47169,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
46829
47169
  listeners.clear();
46830
47170
  peerListeners.clear();
46831
47171
  peers.clear();
47172
+ peersByUid.clear();
46832
47173
  records.clear();
46833
47174
  }
46834
47175
  });
@@ -47089,6 +47430,10 @@ function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onEr
47089
47430
  }
47090
47431
  return Object.freeze({
47091
47432
  getSnapshot: () => candidate || record,
47433
+ async settled() {
47434
+ await tail;
47435
+ return record;
47436
+ },
47092
47437
  async ready() {
47093
47438
  const response = await channel.ready();
47094
47439
  await consume(response.record);
@@ -47427,6 +47772,20 @@ function openAccountCallLease({ cloud, capability: accountCapability, endpoint,
47427
47772
  });
47428
47773
  }
47429
47774
 
47775
+ // ../../core/calls/sources.js
47776
+ function callVideoCapacity(participants, holder, count = 1) {
47777
+ return participants.filter((peer) => peer.holder !== holder).reduce((count2, peer) => count2 + (peer.media?.sources?.length || 0), 0) + count <= CALL_MAX_VIDEO_SOURCES;
47778
+ }
47779
+ function assertCallVideoSources(participants, holder, media) {
47780
+ const sources = media?.sources || [];
47781
+ 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) {
47782
+ throw Object.assign(new Error("invalid call video sources"), { code: "calls/invalid-source" });
47783
+ }
47784
+ if (sources.length && !callVideoCapacity(participants, holder, sources.length)) {
47785
+ throw Object.assign(new Error(`this call already has ${CALL_MAX_VIDEO_SOURCES} cameras or screens`), { code: "calls/video-full" });
47786
+ }
47787
+ }
47788
+
47430
47789
  // ../../core/calls/room.js
47431
47790
  function openCallRoom({
47432
47791
  cloud,
@@ -47513,6 +47872,14 @@ function openCallRoom({
47513
47872
  settleAdmission(admissionError("calls/taken-over", "call moved to another device"));
47514
47873
  }
47515
47874
  }
47875
+ function retireMembership() {
47876
+ departing = true;
47877
+ joined = false;
47878
+ clearTimeout(pulseTimer);
47879
+ disposeState(state);
47880
+ state = null;
47881
+ onRemoved?.();
47882
+ }
47516
47883
  async function adopt(next, admissions) {
47517
47884
  verifyCallMembers(next, admissions, protocol);
47518
47885
  if (closed) {
@@ -47524,7 +47891,7 @@ function openCallRoom({
47524
47891
  disposeState(previous);
47525
47892
  const self = next.members.find((item) => toHex(item.leafId) === localHolder);
47526
47893
  if (!self) {
47527
- onRemoved?.();
47894
+ retireMembership();
47528
47895
  return;
47529
47896
  }
47530
47897
  if (!admissions.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey))
@@ -47598,7 +47965,15 @@ function openCallRoom({
47598
47965
  if (bound)
47599
47966
  leaving.add(packet.holder);
47600
47967
  } else if (packet.kind === "pulse") {
47601
- published.set(packet.holder, packet.data);
47968
+ let media = packet.data.media;
47969
+ try {
47970
+ assertCallVideoSources(participants(), packet.holder, media);
47971
+ } catch {
47972
+ media = published.get(packet.holder)?.media || null;
47973
+ }
47974
+ published.set(packet.holder, { ...packet.data, media });
47975
+ if (packet.holder === localHolder)
47976
+ ownMedia = media;
47602
47977
  } else if (packet.kind === "signal") {
47603
47978
  if (packet.data.to === localHolder && head?.participants.some((item) => item.holder === packet.holder && item.actor === packet.actor)) {
47604
47979
  signals.push(packet);
@@ -47608,7 +47983,7 @@ function openCallRoom({
47608
47983
  membershipChanged = true;
47609
47984
  if (state && !replacement.participants.some((item) => item.holder === localHolder)) {
47610
47985
  settleAdmission(cancelled2());
47611
- onRemoved?.();
47986
+ retireMembership();
47612
47987
  return;
47613
47988
  }
47614
47989
  head = replacement;
@@ -47624,7 +47999,7 @@ function openCallRoom({
47624
47999
  pendingCandidate = null;
47625
48000
  if (next.removed) {
47626
48001
  disposeState(next);
47627
- onRemoved?.();
48002
+ retireMembership();
47628
48003
  return;
47629
48004
  }
47630
48005
  await adopt(next, change.participants);
@@ -47661,7 +48036,7 @@ function openCallRoom({
47661
48036
  verifiedHead = null;
47662
48037
  admittedIdentities.clear();
47663
48038
  if (joined) {
47664
- onRemoved?.();
48039
+ retireMembership();
47665
48040
  return;
47666
48041
  }
47667
48042
  if (submittedJoin)
@@ -47765,7 +48140,7 @@ function openCallRoom({
47765
48140
  for (let attempt = 0;!closed && performance.now() < deadline; attempt += 1) {
47766
48141
  if (kind === "join" && departing)
47767
48142
  throw cancelled2();
47768
- const current = mailbox.getSnapshot();
48143
+ const current = await mailbox.settled();
47769
48144
  if (!current?.head)
47770
48145
  throw kind === "join" ? empty() : new Error("call ended");
47771
48146
  if (kind === "join") {
@@ -47774,9 +48149,14 @@ function openCallRoom({
47774
48149
  throw admissionFailure;
47775
48150
  }
47776
48151
  try {
48152
+ const value = typeof data === "function" ? data() : data;
48153
+ if (kind === "pulse")
48154
+ assertCallVideoSources(participants(), localHolder, value.media);
47777
48155
  if (kind === "join")
47778
48156
  submittedJoin = true;
47779
- return await mailbox.append([protocol.sign(kind, data)], { expectedRevision: current.revision });
48157
+ const result = await mailbox.append([protocol.sign(kind, value)], { expectedRevision: current.revision });
48158
+ await mailbox.settled();
48159
+ return result;
47780
48160
  } catch (error) {
47781
48161
  if (closed)
47782
48162
  throw cancelled2();
@@ -47807,7 +48187,7 @@ function openCallRoom({
47807
48187
  if (closed || departing || !joined)
47808
48188
  return;
47809
48189
  try {
47810
- await event("pulse", { ...audio, media: ownMedia });
48190
+ await event("pulse", () => ({ ...audio, media: ownMedia }));
47811
48191
  } catch (error) {
47812
48192
  fail(error);
47813
48193
  }
@@ -47889,13 +48269,12 @@ function openCallRoom({
47889
48269
  return event("signal", { to, signal });
47890
48270
  },
47891
48271
  async publishMedia(media) {
47892
- ownMedia = media;
47893
- await event("pulse", { ...audio, media });
48272
+ await event("pulse", () => ({ ...audio, media }));
47894
48273
  },
47895
48274
  async setAudio(value) {
47896
48275
  audio = { muted: value.muted === true || value.deafened === true, deafened: value.deafened === true };
47897
48276
  if (joined && !departing && !closed)
47898
- await event("pulse", { ...audio, media: ownMedia });
48277
+ await event("pulse", () => ({ ...audio, media: ownMedia }));
47899
48278
  },
47900
48279
  async leave() {
47901
48280
  if (closed)
@@ -48092,15 +48471,15 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
48092
48471
  const pendingSignals = [];
48093
48472
  const incomingSignals = [];
48094
48473
  function sourcesFor(peer) {
48095
- if (!port.capabilities?.video || !videoCapable(peer) || !Array.isArray(peer.media.sources) || peer.media.sources.length > 2)
48474
+ if (!port.capabilities?.video || !videoCapable(peer) || !Array.isArray(peer.media.sources) || peer.media.sources.length > CALL_MAX_VIDEO_SOURCES_PER_PARTICIPANT)
48096
48475
  return [];
48097
- const sources = peer.media.sources;
48476
+ const sources = peer.media.sources.filter((source) => mode === "group" ? source?.trackName != null : source?.mid != null);
48098
48477
  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)
48099
48478
  return [];
48100
48479
  return sources;
48101
48480
  }
48102
48481
  function videoProjection() {
48103
- const next = [...localSources.values()].map((source) => ({ id: source.id, holder, kind: source.kind, stream: source.stream, local: true }));
48482
+ const next = [...localSources.values()].filter((source) => source.admitted).map((source) => ({ id: source.id, holder, kind: source.kind, stream: source.stream, local: true }));
48104
48483
  for (const peer of peers) {
48105
48484
  if (peer.holder === holder)
48106
48485
  continue;
@@ -48120,11 +48499,11 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
48120
48499
  const base2 = mode === "group" ? { sessionId, trackName: microphoneTrack } : {};
48121
48500
  if (!port.capabilities?.video)
48122
48501
  return base2;
48123
- return { ...base2, capabilities: ["video"], sources: [...localSources.values()].filter((source) => source.published).map((source) => ({
48502
+ return { ...base2, capabilities: ["video"], sources: [...localSources.values()].map((source) => ({
48124
48503
  id: source.id,
48125
48504
  kind: source.kind,
48126
48505
  contextId: source.contextId,
48127
- ...mode === "group" ? { trackName: source.trackName } : { mid: source.mid }
48506
+ ...source.published ? mode === "group" ? { trackName: source.trackName } : { mid: source.mid } : {}
48128
48507
  })) };
48129
48508
  }
48130
48509
  async function publishMedia() {
@@ -48579,7 +48958,7 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
48579
48958
  current(generation);
48580
48959
  if (videoCapable(peer))
48581
48960
  for (const source of localSources.values()) {
48582
- if (source.attached)
48961
+ if (!source.admitted || source.attached)
48583
48962
  continue;
48584
48963
  media.addSource(source);
48585
48964
  source.attached = true;
@@ -48708,68 +49087,101 @@ function createCallMediaSession({ port, mode, holder, preparation, provider, sig
48708
49087
  current();
48709
49088
  videoProjection();
48710
49089
  }
49090
+ function detachSource(source) {
49091
+ localSources.delete(source.id);
49092
+ source.stream.getTracks().forEach((track) => track.stop());
49093
+ if (source.attached)
49094
+ media?.removeSource(source.id);
49095
+ videoProjection();
49096
+ }
49097
+ async function closeSourceTrack(source) {
49098
+ if (mode === "group" && source.published) {
49099
+ await applyDescription(await provider("tracks/close", { sessionId, tracks: [{ mid: source.mid }] }));
49100
+ }
49101
+ }
48711
49102
  return Object.freeze({
48712
- supportsScreen() {
48713
- return port.capabilities?.screen === true && port.capabilities?.video === true;
49103
+ supportsSource(kind) {
49104
+ return port.capabilities?.[kind] === true && port.capabilities?.video === true;
48714
49105
  },
48715
- async addSource(source) {
49106
+ async addSource(source, replaceId = null) {
48716
49107
  current();
48717
49108
  if (!started || !media?.addSource || !port.capabilities?.video)
48718
49109
  throw new Error("video sharing unavailable");
48719
- if (!validSourceId(source?.id) || !["screen", "camera"].includes(source.kind) || !validContext(source.contextId) || sourceContexts.has(source.contextId) || localSources.has(source.id) || localSources.size >= 2)
49110
+ const previous = localSources.get(replaceId);
49111
+ 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)
48720
49112
  throw new Error("invalid call source");
48721
49113
  const generation = connectionGeneration;
48722
- if (mode === "group")
48723
- media.addSource(source);
48724
- const local = { ...source, attached: mode === "group", published: false };
49114
+ if (previous)
49115
+ detachSource(previous);
49116
+ const local = { ...source, attached: false, admitted: false, published: false };
48725
49117
  localSources.set(source.id, local);
48726
49118
  sourceContexts.add(source.contextId);
48727
- videoProjection();
48728
- await updateKeys();
48729
- current(generation);
48730
- return serialize(async () => {
48731
- current(generation);
48732
- if (localSources.get(source.id) !== local)
48733
- return;
48734
- if (mode === "group") {
48735
- const offer = await media.offer();
49119
+ const result = await serialize(async () => {
49120
+ try {
48736
49121
  current(generation);
48737
- local.mid = media.sourceMid(source.id);
48738
- local.trackName = toHex(randomBytes3(16));
48739
- if (!validMid(local.mid))
48740
- throw new Error("video track unavailable");
48741
- const result = await provider("tracks/new", { sessionId, sessionDescription: sdp(offer), tracks: [{ location: "local", mid: local.mid, trackName: local.trackName }] });
49122
+ if (previous)
49123
+ await closeSourceTrack(previous);
49124
+ if (localSources.get(source.id) !== local)
49125
+ return;
49126
+ await publishMedia();
48742
49127
  current(generation);
48743
- if (result?.tracks?.length !== 1 || result.tracks[0].mid !== local.mid || result.tracks[0].trackName !== local.trackName)
48744
- throw new Error("video publication failed");
48745
- await applyDescription(result, true);
49128
+ if (localSources.get(source.id) !== local)
49129
+ return;
49130
+ local.admitted = true;
49131
+ if (mode === "group") {
49132
+ media.addSource(local);
49133
+ local.attached = true;
49134
+ }
49135
+ videoProjection();
49136
+ await updateKeys();
48746
49137
  current(generation);
48747
- local.published = true;
49138
+ if (localSources.get(source.id) !== local)
49139
+ return;
49140
+ if (mode === "group") {
49141
+ const offer = await media.offer();
49142
+ current(generation);
49143
+ local.mid = media.sourceMid(source.id);
49144
+ local.trackName = toHex(randomBytes3(16));
49145
+ if (!validMid(local.mid))
49146
+ throw new Error("video track unavailable");
49147
+ const response = await provider("tracks/new", { sessionId, sessionDescription: sdp(offer), tracks: [{ location: "local", mid: local.mid, trackName: local.trackName }] });
49148
+ current(generation);
49149
+ if (response?.tracks?.length !== 1 || response.tracks[0].mid !== local.mid || response.tracks[0].trackName !== local.trackName)
49150
+ throw new Error("video publication failed");
49151
+ await applyDescription(response, true);
49152
+ current(generation);
49153
+ local.published = true;
49154
+ await publishMedia();
49155
+ } else
49156
+ await negotiate();
49157
+ } catch (error) {
49158
+ if (closed || generation !== connectionGeneration || !["calls/video-full", "calls/invalid-source"].includes(error?.code))
49159
+ throw error;
49160
+ if (localSources.get(source.id) === local)
49161
+ detachSource(local);
49162
+ await updateKeys();
48748
49163
  await publishMedia();
48749
- } else
48750
- await negotiate();
49164
+ await closeSourceTrack(local);
49165
+ return { error };
49166
+ }
48751
49167
  });
49168
+ if (result?.error)
49169
+ throw result.error;
48752
49170
  },
48753
49171
  removeSource(id) {
48754
49172
  current();
48755
49173
  const local = localSources.get(id);
48756
49174
  if (!local)
48757
49175
  return Promise.resolve();
48758
- localSources.delete(id);
48759
- local.stream.getTracks().forEach((track) => track.stop());
48760
- if (local.attached)
48761
- media?.removeSource(id);
48762
- videoProjection();
49176
+ detachSource(local);
48763
49177
  const removedKeys = updateKeys();
48764
49178
  removedKeys.catch(() => {});
48765
49179
  return serialize(async () => {
48766
49180
  await removedKeys;
48767
49181
  current();
48768
49182
  await publishMedia();
48769
- if (mode === "group") {
48770
- if (local.published)
48771
- await applyDescription(await provider("tracks/close", { sessionId, tracks: [{ mid: local.mid }] }));
48772
- } else if (local.attached) {
49183
+ await closeSourceTrack(local);
49184
+ if (mode === "direct" && local.attached) {
48773
49185
  directDirty = true;
48774
49186
  await negotiate();
48775
49187
  }
@@ -49045,15 +49457,17 @@ function openCallObservation({ cloud, capability, endpoint, protocol, onChange,
49045
49457
  }
49046
49458
 
49047
49459
  // ../../core/calls/session.js
49048
- 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 });
49460
+ 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 });
49049
49461
  var cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" });
49050
49462
  var ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" });
49463
+ var RECENT_CALL_HINT_LIMIT = 32;
49051
49464
  var callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
49052
49465
  function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio, saveAudio, diag }) {
49053
49466
  const listeners = new Set;
49054
49467
  const discoveries = new Map;
49055
49468
  const discoveryReads = new Map;
49056
49469
  const observers = new Map;
49470
+ const messageHints = new Map;
49057
49471
  const retiring = new Set;
49058
49472
  let state = initial();
49059
49473
  const confirmedPeerAudio = new Map;
@@ -49061,6 +49475,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49061
49475
  let peerAudioSource = null;
49062
49476
  let audioSource = normalizeCallAudio();
49063
49477
  let pendingAudio = null;
49478
+ let audioSaveTail = Promise.resolve();
49064
49479
  let identity = null;
49065
49480
  let endpoint = null;
49066
49481
  let leaseEndpoint = null;
@@ -49097,7 +49512,27 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49097
49512
  diag?.("call.message.error", { event, code: error?.code || "" });
49098
49513
  });
49099
49514
  }
49100
- const canShareScreen = (call) => !!call && !call.leaving && call.mediaReady && !!mediaPort?.captureScreen && ["connecting", "connected", "waiting"].includes(state.phase) && call.media?.supportsScreen() === true;
49515
+ const captureFor = (kind) => kind === "camera" ? mediaPort?.captureCamera : mediaPort?.captureScreen;
49516
+ const canShare = (call, kind) => !!call && !call.leaving && call.mediaReady && !!captureFor(kind) && ["connecting", "connected", "waiting"].includes(state.phase) && call.media?.supportsSource(kind) === true && callVideoCapacity(call.participants, call.endpoint.holder);
49517
+ const sourceState = (call) => ({
49518
+ sharingScreen: call?.visual?.kind === "screen",
49519
+ sharingCamera: call?.visual?.kind === "camera",
49520
+ screenShareAvailable: canShare(call, "screen"),
49521
+ cameraAvailable: canShare(call, "camera")
49522
+ });
49523
+ async function captureVisual(operation) {
49524
+ while (operation.visual) {
49525
+ const source = operation.visual;
49526
+ try {
49527
+ await source.capture;
49528
+ } catch (error) {
49529
+ if (operation.visual === source)
49530
+ throw error;
49531
+ }
49532
+ if (operation.visual === source)
49533
+ return;
49534
+ }
49535
+ }
49101
49536
  function current(token) {
49102
49537
  if (closed || !identity || token !== generation)
49103
49538
  throw cancelled2();
@@ -49118,17 +49553,29 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49118
49553
  }
49119
49554
  function available() {
49120
49555
  const entry = discoveries.get(focused);
49121
- const value = (chatId, current2) => current2?.descriptor ? {
49122
- chatId,
49123
- callId: current2.descriptor.callId,
49124
- startedAt: current2.descriptor.startedAt,
49125
- participants: current2.descriptor.participants || 1,
49126
- roster: (active?.discovery === current2 ? active.participants : current2.roster).filter((peer) => ![...retiring].some((call) => call.callId === current2.descriptor.callId && call.endpoint.holder === peer.holder))
49127
- } : null;
49556
+ const value = (chatId, current2) => {
49557
+ if (!current2?.descriptor)
49558
+ return null;
49559
+ const roster = (active?.discovery === current2 ? active.participants : current2.roster)?.filter((peer) => ![...retiring].some((call) => call.callId === current2.descriptor.callId && call.endpoint.holder === peer.holder));
49560
+ if (roster?.length === 0)
49561
+ return null;
49562
+ return {
49563
+ chatId,
49564
+ callId: current2.descriptor.callId,
49565
+ startedAt: current2.descriptor.startedAt,
49566
+ participants: roster?.length ?? (current2.descriptor.participants || 1),
49567
+ roster: roster || []
49568
+ };
49569
+ };
49128
49570
  for (const observer of observers.values()) {
49571
+ const observed = discoveries.get(observer.chatId);
49572
+ if (!observer.resolved && observed?.revision !== null && observed?.revision !== undefined) {
49573
+ observer.resolved = true;
49574
+ diag?.("call.discovery.observed", { elapsedMs: Date.now() - observer.startedAt, active: !!observed.descriptor });
49575
+ }
49129
49576
  if (!observer.onAvailable)
49130
49577
  continue;
49131
- const next2 = value(observer.chatId, discoveries.get(observer.chatId));
49578
+ const next2 = value(observer.chatId, observed);
49132
49579
  const fingerprint = JSON.stringify(next2);
49133
49580
  if (observer.fingerprint === fingerprint)
49134
49581
  continue;
@@ -49164,7 +49611,6 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49164
49611
  return;
49165
49612
  }
49166
49613
  entry.observation?.close();
49167
- entry.roster = [];
49168
49614
  entry.observedCallId = callId;
49169
49615
  const token = generation;
49170
49616
  const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint, callId });
@@ -49195,7 +49641,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49195
49641
  }
49196
49642
  function prune() {
49197
49643
  for (const [chatId, value] of discoveries) {
49198
- if (![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call) => call.chatId === chatId)) {
49644
+ 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((call) => call.chatId === chatId)) {
49199
49645
  closeDiscovery(value);
49200
49646
  discoveries.delete(chatId);
49201
49647
  } else
@@ -49217,7 +49663,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49217
49663
  mailbox: null,
49218
49664
  observation: null,
49219
49665
  observedCallId: null,
49220
- roster: [],
49666
+ roster: null,
49221
49667
  revision: null,
49222
49668
  expiredCallId: null,
49223
49669
  finishing: new Map
@@ -49234,17 +49680,25 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49234
49680
  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))
49235
49681
  throw new Error("invalid call discovery");
49236
49682
  const previous = entry.descriptor;
49683
+ if (previous?.callId !== descriptor?.callId)
49684
+ entry.roster = null;
49237
49685
  entry.expiredCallId = !record.head ? previous?.callId || entry.expiredCallId : null;
49238
49686
  entry.descriptor = descriptor;
49239
49687
  if (!record.head && previous)
49240
49688
  finishDiscovery(entry, previous);
49241
49689
  const changed = entry.revision !== record.revision;
49242
49690
  entry.revision = record.revision;
49691
+ if (!descriptor) {
49692
+ const hint = messageHints.get(chatId);
49693
+ if (hint)
49694
+ hint.watch = false;
49695
+ }
49243
49696
  if (active?.discovery === entry && active.published && descriptor?.callId !== active.callId) {
49244
49697
  leave();
49245
49698
  }
49246
49699
  observeRoster(entry, changed);
49247
49700
  available();
49701
+ prune();
49248
49702
  },
49249
49703
  onError(error) {
49250
49704
  if (token !== generation || discoveries.get(chatId) !== entry)
@@ -49291,7 +49745,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49291
49745
  function observe(chatId, onAvailable) {
49292
49746
  if (!chatId || closed)
49293
49747
  return () => {};
49294
- const observer = { chatId, onAvailable, fingerprint: undefined };
49748
+ const observer = { chatId, onAvailable, fingerprint: undefined, startedAt: Date.now(), resolved: false };
49295
49749
  observers.set(observer, observer);
49296
49750
  focused = chatId;
49297
49751
  if (state.error?.chatId === chatId)
@@ -49343,11 +49797,11 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49343
49797
  })();
49344
49798
  const destination = (async () => {
49345
49799
  if (!onlyExisting && !continuation)
49346
- chatId = await chat.getSnapshot().materializeChat(chatId);
49800
+ chatId = await chat.getSnapshot().materializeChat(chatId, { signal: operation.controller.signal });
49347
49801
  check();
49348
49802
  joining.chatId = chatId;
49349
49803
  step("chat");
49350
- const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId);
49804
+ const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId, { signal: operation.controller.signal });
49351
49805
  check();
49352
49806
  if (!epochState)
49353
49807
  throw cancelled2();
@@ -49356,7 +49810,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49356
49810
  step("discovery");
49357
49811
  return entry2;
49358
49812
  })();
49359
- const [, entry] = await Promise.all([permission, destination, operation.screen?.capture]);
49813
+ const [, entry] = await Promise.all([permission, destination, captureVisual(operation)]);
49360
49814
  check();
49361
49815
  const discoveryRecord = entry.mailbox.getSnapshot();
49362
49816
  const create = !entry.descriptor;
@@ -49392,7 +49846,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49392
49846
  wasReady: false,
49393
49847
  signalingReady: true,
49394
49848
  ownershipReady: true,
49395
- screen: null,
49849
+ visual: null,
49850
+ capturing: null,
49396
49851
  sourceContext: 0,
49397
49852
  leaving: false,
49398
49853
  muteTail: Promise.resolve()
@@ -49438,7 +49893,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49438
49893
  const waiting = call.mediaReady && descriptor.mode === "direct" && state.participants.length < 2;
49439
49894
  const ready = call.mediaState === "connected" || waiting;
49440
49895
  if (ready && call.signalingReady && call.ownershipReady) {
49441
- call.wasReady = true;
49896
+ call.wasReady = !waiting;
49442
49897
  clearTimeout(call.joinTimer);
49443
49898
  call.joinTimer = null;
49444
49899
  emit({ phase: waiting ? "waiting" : "connected" });
@@ -49452,7 +49907,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49452
49907
  fail(new Error("call did not connect"));
49453
49908
  }, 30000);
49454
49909
  }
49455
- emit({ screenShareAvailable: canShareScreen(call) });
49910
+ emit(sourceState(call));
49456
49911
  };
49457
49912
  call.connectionProgress = connectionProgress;
49458
49913
  call.media = createCallMediaSession({
@@ -49499,8 +49954,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49499
49954
  emit({ videoSources });
49500
49955
  },
49501
49956
  onSourceEnded(id) {
49502
- if (valid() && call.screen?.id === id)
49503
- stopScreenShare().catch(() => {});
49957
+ if (valid() && call.visual?.id === id)
49958
+ stopVisual().catch(() => {});
49504
49959
  },
49505
49960
  onError(error) {
49506
49961
  if (valid())
@@ -49545,7 +50000,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49545
50000
  if (preference)
49546
50001
  call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
49547
50002
  }
49548
- emit({ participants: value, screenShareAvailable: canShareScreen(call) });
50003
+ emit({ participants: value, ...sourceState(call) });
49549
50004
  available();
49550
50005
  call.refresh?.();
49551
50006
  if (call.mediaReady)
@@ -49576,6 +50031,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49576
50031
  checkCall();
49577
50032
  await call.room.start({ create });
49578
50033
  checkCall();
50034
+ if (operation.visual)
50035
+ assertCallVideoSources(call.participants, callEndpoint.holder, { sources: [{ ...operation.visual, contextId: 1 }] });
49579
50036
  step("admission");
49580
50037
  if (create) {
49581
50038
  try {
@@ -49677,12 +50134,14 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49677
50134
  };
49678
50135
  call.refresh = refresh;
49679
50136
  refresh(true);
49680
- if (operation.screen) {
49681
- await operation.screen.capture;
50137
+ while (operation.visual) {
50138
+ await captureVisual(operation);
49682
50139
  check();
49683
- await publishScreen(call, operation.screen);
50140
+ const source = operation.visual;
50141
+ await publishVisual(call, source);
49684
50142
  check();
49685
- operation.screen = null;
50143
+ if (operation.visual === source)
50144
+ operation.visual = null;
49686
50145
  }
49687
50146
  return chatId;
49688
50147
  } catch (error) {
@@ -49715,20 +50174,21 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49715
50174
  throw new Error("open veyl to join a call");
49716
50175
  return runJoin(chatId, expectedCallId, onlyExisting);
49717
50176
  }
49718
- function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null, screen = null) {
50177
+ function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null, visual = null) {
49719
50178
  const intent = ++joinIntent;
49720
- const operation = { predecessor: active, chatId, candidate: null, continuation, screen };
50179
+ const operation = { predecessor: active, chatId, candidate: null, continuation, visual, controller: new AbortController };
49721
50180
  let resolve, reject;
49722
50181
  operation.promise = new Promise((done, fail2) => {
49723
50182
  resolve = done;
49724
50183
  reject = fail2;
49725
50184
  });
49726
- if (screen)
49727
- screen.promise = operation.promise;
50185
+ if (visual)
50186
+ visual.promise = operation.promise;
49728
50187
  const previous = joining;
49729
50188
  joining = operation;
50189
+ previous?.controller.abort();
49730
50190
  previous?.preparation?.close();
49731
- previous?.screen?.stop();
50191
+ previous?.visual?.stop();
49732
50192
  emit({ joiningChatId: chatId });
49733
50193
  if (previous?.candidate && previous.candidate !== active)
49734
50194
  disposeCall(previous.candidate);
@@ -49752,7 +50212,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49752
50212
  }
49753
50213
  }
49754
50214
  } finally {
49755
- operation.screen?.stop();
50215
+ operation.controller.abort();
50216
+ operation.visual?.stop();
49756
50217
  if (joining === operation) {
49757
50218
  joining = null;
49758
50219
  emit({ joiningChatId: null });
@@ -49764,8 +50225,9 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49764
50225
  }
49765
50226
  function leave() {
49766
50227
  joinIntent += 1;
50228
+ joining?.controller.abort();
49767
50229
  joining?.preparation?.close();
49768
- joining?.screen?.stop();
50230
+ joining?.visual?.stop();
49769
50231
  const candidate = joining?.candidate;
49770
50232
  joining = null;
49771
50233
  emit({ joiningChatId: null });
@@ -49780,8 +50242,9 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49780
50242
  if (!operation)
49781
50243
  return Promise.resolve();
49782
50244
  joinIntent += 1;
50245
+ operation.controller.abort();
49783
50246
  operation.preparation?.close();
49784
- operation.screen?.stop();
50247
+ operation.visual?.stop();
49785
50248
  joining = null;
49786
50249
  emit({ joiningChatId: null });
49787
50250
  const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
@@ -49800,15 +50263,17 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49800
50263
  leaving = retired.length ? Promise.race([released, Promise.all(retired.map((room) => room.closing))]) : released;
49801
50264
  }
49802
50265
  if (!call) {
49803
- emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false });
50266
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false, sharingCamera: false, cameraAvailable: false });
49804
50267
  return leaving;
49805
50268
  }
50269
+ if (call.discovery.descriptor?.callId === call.callId)
50270
+ call.discovery.roster = call.participants;
49806
50271
  active = null;
49807
50272
  leaving = disposeCall(call);
49808
50273
  if (identity && discoveries.get(call.chatId) === call.discovery)
49809
50274
  observeRoster(call.discovery);
49810
50275
  available();
49811
- emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false });
50276
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [], videoSources: [], sharingScreen: false, screenShareAvailable: false, sharingCamera: false, cameraAvailable: false });
49812
50277
  return leaving;
49813
50278
  }
49814
50279
  async function retireDiscovery(call) {
@@ -49829,15 +50294,20 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49829
50294
  function finishDiscovery(entry, descriptor) {
49830
50295
  if (entry.finishing.has(descriptor.callId))
49831
50296
  return;
49832
- 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));
50297
+ 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(() => {
50298
+ entry.finishing.delete(descriptor.callId);
50299
+ prune();
50300
+ });
49833
50301
  entry.finishing.set(descriptor.callId, finishing);
49834
50302
  }
49835
50303
  function disposeCall(call) {
49836
50304
  if (call.closing)
49837
50305
  return call.closing;
49838
50306
  call.leaving = true;
49839
- call.screen?.stop();
49840
- call.screen = null;
50307
+ call.capturing?.stop();
50308
+ call.capturing = null;
50309
+ call.visual?.stop();
50310
+ call.visual = null;
49841
50311
  retiring.add(call);
49842
50312
  clearTimeout(call.timer);
49843
50313
  clearTimeout(call.joinTimer);
@@ -49879,28 +50349,40 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49879
50349
  });
49880
50350
  return call.closing;
49881
50351
  }
49882
- function startScreenShare(chatId = active?.chatId) {
49883
- if (!identity || closed || !mediaPort?.captureScreen || !mediaPort.capabilities?.screen || !mediaPort.capabilities?.video || !mls || !cloud.calls || !chatId)
49884
- return Promise.reject(new Error("screen sharing is unavailable in this call"));
50352
+ function startVisual(kind, chatId = active?.chatId) {
50353
+ if (!identity || closed || !captureFor(kind) || !mediaPort.capabilities?.[kind] || !mediaPort.capabilities?.video || !mls || !cloud.calls || !chatId)
50354
+ return Promise.reject(new Error(`${kind === "camera" ? "camera" : "screen sharing"} is unavailable in this call`));
49885
50355
  const operation = joining && [joining.chatId, state.joiningChatId].includes(chatId) ? joining : null;
49886
- if (operation?.screen)
49887
- return operation.screen.promise;
49888
50356
  const call = active?.chatId === chatId ? active : null;
49889
- if (call && !operation && !canShareScreen(call))
49890
- return Promise.reject(new Error("screen sharing is unavailable in this call"));
50357
+ const pending = operation?.visual || call?.capturing;
50358
+ if (pending?.kind === kind)
50359
+ return pending.promise;
50360
+ if (!pending && call?.visual?.kind === kind)
50361
+ return call.visual.promise;
50362
+ if (call && !operation && !canShare(call, kind)) {
50363
+ try {
50364
+ assertCallVideoSources(call.participants, call.endpoint.holder, { sources: [{ id: "capture", kind, contextId: 1 }] });
50365
+ } catch (error) {
50366
+ return Promise.reject(error);
50367
+ }
50368
+ return Promise.reject(new Error(`${kind} is unavailable in this call`));
50369
+ }
49891
50370
  if (!call && !foreground)
49892
50371
  return Promise.reject(new Error("open veyl to join a call"));
49893
- if (call?.screen)
49894
- return call.screen.promise;
50372
+ let rejectCapture;
50373
+ const cancellation = new Promise((_, reject) => {
50374
+ rejectCapture = reject;
50375
+ });
49895
50376
  const source = {
49896
50377
  id: toHex(randomBytes3(16)),
49897
50378
  contextId: null,
49898
- kind: "screen",
50379
+ kind,
49899
50380
  stream: null,
49900
50381
  promise: null,
49901
50382
  cancelled: false,
49902
50383
  stop() {
49903
50384
  source.cancelled = true;
50385
+ rejectCapture(cancelled2());
49904
50386
  for (const track of source.stream?.getTracks() || []) {
49905
50387
  track.removeEventListener?.("ended", sourceEnded);
49906
50388
  track.stop();
@@ -49908,18 +50390,18 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49908
50390
  }
49909
50391
  };
49910
50392
  const sourceEnded = () => {
49911
- if (joining?.screen === source)
50393
+ if (joining?.visual === source)
49912
50394
  cancelJoin();
49913
- else if (active?.screen === source)
49914
- stopScreenShare().catch(() => {});
50395
+ else if (active?.visual === source || active?.capturing === source)
50396
+ stopVisual().catch(() => {});
49915
50397
  };
49916
50398
  let capturing;
49917
50399
  try {
49918
- capturing = mediaPort.captureScreen();
50400
+ capturing = captureFor(kind).call(mediaPort);
49919
50401
  } catch (error) {
49920
50402
  return Promise.reject(error);
49921
50403
  }
49922
- source.capture = Promise.resolve(capturing).then((stream) => {
50404
+ source.capture = Promise.race([cancellation, Promise.resolve(capturing).then((stream) => {
49923
50405
  if (source.cancelled || stream.getTracks().some((track) => track.readyState === "ended")) {
49924
50406
  stream.getTracks().forEach((track) => track.stop());
49925
50407
  throw cancelled2();
@@ -49927,64 +50409,78 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49927
50409
  source.stream = stream;
49928
50410
  for (const track of stream.getTracks())
49929
50411
  track.addEventListener?.("ended", sourceEnded, { once: true });
49930
- });
50412
+ })]);
49931
50413
  if (operation) {
50414
+ operation.visual = source;
50415
+ if (pending !== call?.visual)
50416
+ pending?.stop();
49932
50417
  source.promise = Promise.all([source.capture, operation.promise]).then(() => {
49933
50418
  return;
49934
50419
  });
49935
- operation.screen = source;
49936
50420
  return source.promise;
49937
50421
  }
49938
50422
  if (!call) {
49939
50423
  source.promise = runJoin(chatId, null, false, null, source);
49940
50424
  return source.promise;
49941
50425
  }
49942
- call.screen = source;
50426
+ call.capturing = source;
50427
+ if (pending !== call.visual)
50428
+ pending?.stop();
49943
50429
  source.promise = source.capture.then(() => {
49944
- if (!canShareScreen(call))
50430
+ if (source.cancelled || active !== call || call.capturing !== source || call.leaving || !call.mediaReady)
49945
50431
  throw cancelled2();
49946
- return publishScreen(call, source);
50432
+ return publishVisual(call, source);
49947
50433
  }).catch(async (error) => {
49948
50434
  source.stop();
49949
- if (call.screen === source) {
49950
- call.screen = null;
50435
+ if (call.visual === source) {
50436
+ call.visual = null;
49951
50437
  if (active === call)
49952
- emit({ sharingScreen: false });
50438
+ emit(sourceState(call));
49953
50439
  if (!call.leaving && source.stream)
49954
50440
  await call.media.removeSource(source.id).catch(() => {});
49955
50441
  }
49956
50442
  throw error;
50443
+ }).finally(() => {
50444
+ if (call.capturing === source)
50445
+ call.capturing = null;
49957
50446
  });
49958
50447
  return source.promise;
49959
50448
  }
49960
- async function publishScreen(call, source) {
49961
- if (source.cancelled || active !== call || call.leaving || !call.mediaReady || !call.media.supportsScreen())
50449
+ async function publishVisual(call, source) {
50450
+ if (source.cancelled || active !== call || call.leaving || !call.mediaReady || !call.media.supportsSource(source.kind))
49962
50451
  throw cancelled2();
50452
+ const previous = call.visual;
49963
50453
  source.contextId = ++call.sourceContext;
49964
- call.screen = source;
49965
- emit({ sharingScreen: true });
49966
- await call.media.addSource(source);
49967
- if (active !== call || call.screen !== source)
50454
+ previous?.stop();
50455
+ call.visual = source;
50456
+ emit(sourceState(call));
50457
+ await call.media.addSource(source, previous?.id);
50458
+ if (active !== call || call.visual !== source)
49968
50459
  throw cancelled2();
49969
50460
  }
49970
- async function stopScreenShare() {
49971
- if (joining?.screen)
50461
+ async function stopVisual(kind) {
50462
+ if (joining?.visual && (!kind || joining.visual.kind === kind))
49972
50463
  return cancelJoin();
49973
50464
  const call = active;
49974
- const source = call?.screen;
49975
- if (!source)
50465
+ if (call?.capturing && (!kind || call.capturing.kind === kind || call.visual?.kind === kind)) {
50466
+ call.capturing.stop();
50467
+ call.capturing = null;
50468
+ }
50469
+ const source = call?.visual;
50470
+ if (!source || kind && source.kind !== kind)
49976
50471
  return;
49977
- call.screen = null;
50472
+ call.visual = null;
49978
50473
  source.stop();
49979
- emit({ sharingScreen: false });
50474
+ emit(sourceState(call));
49980
50475
  if (source.stream)
49981
50476
  await call.media.removeSource(source.id);
49982
50477
  }
49983
50478
  function applyAudio(call) {
49984
50479
  const audio = { muted: state.muted || state.deafened, deafened: state.deafened };
49985
50480
  const applied = Promise.all([call.media.mute(audio.muted), call.media.deafen(audio.deafened)]);
50481
+ call.pendingAudio = audio;
49986
50482
  const result = Promise.all([applied, call.muteTail]).then(() => {
49987
- if (!call.leaving)
50483
+ if (!call.leaving && call.pendingAudio === audio)
49988
50484
  return call.room.setAudio(audio);
49989
50485
  });
49990
50486
  call.muteTail = result.catch(() => {});
@@ -49996,14 +50492,16 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
49996
50492
  return;
49997
50493
  const token = generation;
49998
50494
  pendingAudio = value;
49999
- const saving = Promise.resolve().then(() => {
50495
+ const saving = audioSaveTail.then(async () => {
50000
50496
  if (generation !== token)
50001
50497
  throw cancelled2();
50002
- return saveAudio(value);
50003
- }).then(() => {
50498
+ if (pendingAudio !== value)
50499
+ return;
50500
+ await saveAudio(value);
50004
50501
  if (generation === token)
50005
50502
  audioSource = value;
50006
50503
  });
50504
+ audioSaveTail = saving.catch(() => {});
50007
50505
  try {
50008
50506
  await Promise.all([saving, applyAudioPreferences(value)]);
50009
50507
  } finally {
@@ -50174,6 +50672,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50174
50672
  endpoint = null;
50175
50673
  discoveries.clear();
50176
50674
  discoveryReads.clear();
50675
+ messageHints.clear();
50177
50676
  identity = session;
50178
50677
  const stopped = leave();
50179
50678
  Promise.resolve(stopped).finally(() => {
@@ -50188,6 +50687,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50188
50687
  confirmedPeerAudio.clear();
50189
50688
  pendingPeerAudio.clear();
50190
50689
  pendingAudio = null;
50690
+ audioSaveTail = Promise.resolve();
50191
50691
  audioSource = normalizeCallAudio(settings.callAudio);
50192
50692
  peerAudioSource = session ? peerAudio : null;
50193
50693
  for (const [chatPK, value] of Object.entries(peerAudioSource || {}))
@@ -50205,10 +50705,41 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50205
50705
  return retirement;
50206
50706
  }
50207
50707
  function refreshObserved() {
50208
- for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
50708
+ for (const chatId of observedChatIds()) {
50209
50709
  readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
50210
50710
  }
50211
50711
  }
50712
+ function observedChatIds() {
50713
+ return new Set([...observers.values()].map((item) => item.chatId).concat([...messageHints].filter(([, hint]) => hint.watch).map(([chatId]) => chatId)));
50714
+ }
50715
+ const unsubscribeMessages = chat.subscribeMessages((chatId, messages) => {
50716
+ if (!identity || closed)
50717
+ return;
50718
+ let latest = null;
50719
+ for (const message of messages) {
50720
+ if (!isServerConfirmedMsg(message) || !readCallMessage(message))
50721
+ continue;
50722
+ if (!latest || getMessageOrderMs(message) > getMessageOrderMs(latest) || getMessageOrderMs(message) === getMessageOrderMs(latest) && getMessageKey(message) > getMessageKey(latest))
50723
+ latest = message;
50724
+ }
50725
+ if (!latest)
50726
+ return;
50727
+ const at = getMessageOrderMs(latest), key = getMessageKey(latest);
50728
+ if (!Number.isFinite(at) || !key)
50729
+ return;
50730
+ const previous = messageHints.get(chatId);
50731
+ if (previous && (previous.at > at || previous.at === at && previous.key >= key))
50732
+ return;
50733
+ const entry = discoveries.get(chatId);
50734
+ const watch = latest.event === "started" || previous?.watch === true || !!entry?.descriptor;
50735
+ messageHints.delete(chatId);
50736
+ messageHints.set(chatId, { at, key, watch });
50737
+ while (messageHints.size > RECENT_CALL_HINT_LIMIT)
50738
+ messageHints.delete(messageHints.keys().next().value);
50739
+ prune();
50740
+ if (watch && online)
50741
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "message", code: error?.code || "calls/discovery" }));
50742
+ });
50212
50743
  const unsubscribe = chat.subscribe(() => {
50213
50744
  if (!identity)
50214
50745
  return;
@@ -50222,6 +50753,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50222
50753
  const intent = ++joinIntent;
50223
50754
  const previous = active || joining?.candidate;
50224
50755
  active = null;
50756
+ joining?.controller.abort();
50225
50757
  joining = null;
50226
50758
  if (previous)
50227
50759
  disposeCall(previous);
@@ -50233,6 +50765,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50233
50765
  videoSources: [],
50234
50766
  sharingScreen: false,
50235
50767
  screenShareAvailable: false,
50768
+ sharingCamera: false,
50769
+ cameraAvailable: false,
50236
50770
  participants: state.participants.filter((member) => members.has(member.chatPK))
50237
50771
  });
50238
50772
  diag?.("call.epoch.transition", { stage: "starting", participants: state.participants.length });
@@ -50251,11 +50785,11 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50251
50785
  closeDiscovery(entry);
50252
50786
  discoveries.delete(chatId);
50253
50787
  available();
50254
- if (online && [...observers.values()].some((item) => item.chatId === chatId))
50788
+ if (online && observedChatIds().has(chatId))
50255
50789
  readDiscovery(chatId).catch(() => {});
50256
50790
  }
50257
50791
  if (online)
50258
- for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
50792
+ for (const chatId of observedChatIds()) {
50259
50793
  if (!discoveries.has(chatId) && !discoveryReads.has(chatId) && chat.getSnapshot().getOwnerChat(chatId)?.epochState) {
50260
50794
  readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
50261
50795
  }
@@ -50273,8 +50807,10 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50273
50807
  leave,
50274
50808
  cancelJoin,
50275
50809
  setMicrophoneVolume,
50276
- startScreenShare,
50277
- stopScreenShare,
50810
+ startScreenShare: (chatId) => startVisual("screen", chatId),
50811
+ stopScreenShare: () => stopVisual("screen"),
50812
+ startCamera: (chatId) => startVisual("camera", chatId),
50813
+ stopCamera: () => stopVisual("camera"),
50278
50814
  async resolveActiveCall(chatId) {
50279
50815
  const entry = await readDiscovery(chatId);
50280
50816
  return entry.descriptor?.callId || null;
@@ -50289,6 +50825,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50289
50825
  return Promise.reject(ended());
50290
50826
  return join(chatId, callId, true);
50291
50827
  },
50828
+ setAudio,
50292
50829
  setMuted(value) {
50293
50830
  return setAudio({ muted: value === true });
50294
50831
  },
@@ -50322,6 +50859,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio,
50322
50859
  });
50323
50860
  closed = true;
50324
50861
  const stopped = setSources(null);
50862
+ unsubscribeMessages();
50325
50863
  unsubscribe();
50326
50864
  listeners.clear();
50327
50865
  Promise.resolve(stopped).finally(() => Promise.all([mls?.close?.(), mediaPort?.close?.()])).then(finish, reject);
@@ -53524,6 +54062,18 @@ function stablePeerMap(previous, next) {
53524
54062
  }
53525
54063
  return previous;
53526
54064
  }
54065
+ function replaceIndexedPeers(index, updates, field) {
54066
+ let next = index;
54067
+ for (const peer of updates.values()) {
54068
+ const key = peer[field];
54069
+ if (!key || index.get(key)?.uid !== peer.uid)
54070
+ continue;
54071
+ if (next === index)
54072
+ next = new Map(index);
54073
+ next.set(key, peer);
54074
+ }
54075
+ return next;
54076
+ }
53527
54077
  function stableValueSet(previous, next) {
53528
54078
  if (previous === next)
53529
54079
  return previous;
@@ -53551,12 +54101,11 @@ function peerProfileSnapshot(peer) {
53551
54101
  active: !!peer.active,
53552
54102
  presence: peer.presence,
53553
54103
  accountType: peer.accountType || null,
53554
- blocked: !!peer.blocked,
53555
- hidden: !!peer.hidden
54104
+ blocked: !!peer.blocked
53556
54105
  };
53557
54106
  }
53558
54107
  function samePeerProfile(left, right) {
53559
- 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;
54108
+ 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;
53560
54109
  }
53561
54110
  function normalizeProfileIdentity(identity = {}) {
53562
54111
  const uid = String(identity?.uid || "").trim();
@@ -53646,7 +54195,6 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53646
54195
  let blockedUids = [];
53647
54196
  let blockedUidsKey = "";
53648
54197
  let blockedUidSet = new Set;
53649
- let hiddenPeerUidSet = new Set;
53650
54198
  let seenWalletPKs = new Set;
53651
54199
  let seenChatPKs = new Set;
53652
54200
  let walletPKs = [];
@@ -53680,7 +54228,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53680
54228
  subscriberCount += 1;
53681
54229
  if (!firstSubscriber)
53682
54230
  return;
53683
- unsubscribePresence = presence.subscribePeers(() => rebuild({ projection: true, persist: false }));
54231
+ unsubscribePresence = presence.subscribePeers(updatePresence);
53684
54232
  hydrateLocalAvatars();
53685
54233
  const becameReady = startProfileLoad();
53686
54234
  if (becameReady) {
@@ -53712,7 +54260,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53712
54260
  function resolveProfile(identity) {
53713
54261
  if (!identity)
53714
54262
  return null;
53715
- 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;
54263
+ 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;
53716
54264
  return peer?.uid ? peerProfilesByUid.get(peer.uid) || null : null;
53717
54265
  }
53718
54266
  function emitProfiles() {
@@ -53743,8 +54291,6 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53743
54291
  primePeer,
53744
54292
  findPeer,
53745
54293
  updatePeer,
53746
- dropPeer,
53747
- restorePeer,
53748
54294
  loadBlockedPeers
53749
54295
  };
53750
54296
  }
@@ -53822,7 +54368,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53822
54368
  function syncPresencePeers() {
53823
54369
  const candidates = new Map;
53824
54370
  const add2 = (profile) => {
53825
- if (!profile?.uid || profile.blocked || profile.hidden || candidates.size >= RECENT_PEER_REFRESH_LIMIT)
54371
+ if (!profile?.uid || profile.blocked || candidates.size >= RECENT_PEER_REFRESH_LIMIT)
53826
54372
  return;
53827
54373
  candidates.set(profile.uid, profile);
53828
54374
  };
@@ -53846,33 +54392,61 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53846
54392
  function buildBasePeers() {
53847
54393
  allPeers = assemblePeers(walletPeerSet, sources.chatPeers, discoveredPeerUids);
53848
54394
  }
53849
- function buildPeerProjection() {
53850
- const blockedPeerUidSet = new Set(blockedUids);
53851
- for (const peer of blockedPeers) {
53852
- if (peer?.uid)
53853
- blockedPeerUidSet.add(peer.uid);
54395
+ function projectPeer(peer) {
54396
+ const activity = presence.getPeer(peer);
54397
+ const next = {
54398
+ ...peer,
54399
+ active: activity.availability === "online",
54400
+ presence: activity,
54401
+ blocked: blockedUidSet.has(peer.uid)
54402
+ };
54403
+ const previous = allPeerByUid.get(peer.uid);
54404
+ return samePeerRecord(previous, next) ? previous : next;
54405
+ }
54406
+ function updatePresence(changedUids) {
54407
+ const updates = new Map;
54408
+ for (const uid of changedUids) {
54409
+ const previous = allPeerByUid.get(uid);
54410
+ if (!previous)
54411
+ continue;
54412
+ const next = projectPeer(previous);
54413
+ if (next !== previous)
54414
+ updates.set(uid, next);
54415
+ }
54416
+ if (!updates.size)
54417
+ return;
54418
+ allPeerByUid = replaceIndexedPeers(allPeerByUid, updates, "uid");
54419
+ peerByUid = replaceIndexedPeers(peerByUid, updates, "uid");
54420
+ peerByWalletPK = replaceIndexedPeers(peerByWalletPK, updates, "walletPK");
54421
+ peerByChatPK = replaceIndexedPeers(peerByChatPK, updates, "chatPK");
54422
+ peerByUsername = replaceIndexedPeers(peerByUsername, updates, "username");
54423
+ const replaceList = (list) => stablePeerList(list, list.map((peer) => updates.get(peer.uid) || peer));
54424
+ visiblePeers = replaceList(visiblePeers);
54425
+ blockedPeerItems = replaceList(blockedPeerItems);
54426
+ const all = replaceList(recentPeers.all);
54427
+ const wallet = replaceList(recentPeers.wallet);
54428
+ const chat = replaceList(recentPeers.chat);
54429
+ if (all !== recentPeers.all || wallet !== recentPeers.wallet || chat !== recentPeers.chat)
54430
+ recentPeers = { all, wallet, chat };
54431
+ peerProfilesByUid = new Map(peerProfilesByUid);
54432
+ for (const peer of updates.values()) {
54433
+ const next = peerProfileSnapshot(peer);
54434
+ const previous = peerProfilesByUid.get(peer.uid);
54435
+ peerProfilesByUid.set(peer.uid, samePeerProfile(previous, next) ? previous : next);
53854
54436
  }
54437
+ publish();
54438
+ }
54439
+ function buildPeerProjection() {
53855
54440
  const peerMap = new Map;
53856
54441
  for (const peer of allPeers)
53857
54442
  mergePeerRecord(peerMap, peer);
53858
54443
  for (const peer of blockedPeers)
53859
54444
  mergePeerRecord(peerMap, peer);
53860
- const taggedPeers = Array.from(peerMap.values()).map((peer) => {
53861
- const activity = presence.getPeer(peer);
53862
- const next = {
53863
- ...peer,
53864
- active: activity.availability === "online",
53865
- presence: activity,
53866
- blocked: blockedPeerUidSet.has(peer.uid) || hiddenPeerUidSet.has(peer.uid),
53867
- hidden: hiddenPeerUidSet.has(peer.uid)
53868
- };
53869
- const previous = allPeerByUid.get(peer.uid);
53870
- return samePeerRecord(previous, next) ? previous : next;
53871
- });
53872
- visiblePeers = stablePeerList(visiblePeers, taggedPeers.filter((peer) => !peer.blocked && !peer.hidden));
54445
+ const taggedPeers = Array.from(peerMap.values()).map(projectPeer);
54446
+ visiblePeers = stablePeerList(visiblePeers, taggedPeers.filter((peer) => !peer.blocked));
53873
54447
  const blockedByUid = new Map(taggedPeers.filter((peer) => peer?.uid && peer.blocked).map((peer) => [peer.uid, peer]));
53874
54448
  const orderedBlocked = blockedUids.map((uid) => blockedByUid.get(uid)).filter((peer) => peer?.uid);
53875
- blockedPeerItems = stablePeerList(blockedPeerItems, orderedBlocked.length || blockedUids.length ? orderedBlocked : Array.from(blockedByUid.values()));
54449
+ blockedPeerItems = stablePeerList(blockedPeerItems, orderedBlocked);
53876
54450
  blockedChatPKSet = stableValueSet(blockedChatPKSet, new Set(taggedPeers.filter((peer) => peer.blocked && peer.chatPK).map((peer) => peer.chatPK)));
53877
54451
  const allIndexes = indexPeers(taggedPeers);
53878
54452
  const visibleIndexes = indexPeers(visiblePeers);
@@ -53985,7 +54559,6 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
53985
54559
  blockedPeersReady = false;
53986
54560
  discoveredPeerUids = new Set;
53987
54561
  blockedPeers = [];
53988
- hiddenPeerUidSet = new Set;
53989
54562
  seenWalletPKs = new Set;
53990
54563
  seenChatPKs = new Set;
53991
54564
  allPeers = [];
@@ -54098,6 +54671,10 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
54098
54671
  blockedUids = nextBlockedUids;
54099
54672
  blockedUidsKey = nextBlockedUidsKey;
54100
54673
  blockedUidSet = new Set(blockedUids);
54674
+ for (const uid of blockedUids) {
54675
+ if (allPeerByUid.has(uid))
54676
+ discoveredPeerUids.add(uid);
54677
+ }
54101
54678
  if (blockedPeersReady) {
54102
54679
  blockedPeers = blockedPeers.filter((peer) => blockedUidSet.has(peer?.uid));
54103
54680
  }
@@ -54139,7 +54716,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
54139
54716
  const enrichedPeer = await fetchAndCachePeer(partialProfile);
54140
54717
  if (!isCurrent(session) || !enrichedPeer)
54141
54718
  return null;
54142
- if (blockedUidSet.has(enrichedPeer.uid) || hiddenPeerUidSet.has(enrichedPeer.uid))
54719
+ if (blockedUidSet.has(enrichedPeer.uid))
54143
54720
  return null;
54144
54721
  if (!discoveredPeerUids.has(enrichedPeer.uid)) {
54145
54722
  discoveredPeerUids.add(enrichedPeer.uid);
@@ -54198,37 +54775,14 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
54198
54775
  if (requestedKey !== blockedUidsKey)
54199
54776
  continue;
54200
54777
  blockedPeers = requestedUids.map((uid) => nextByUid.get(uid)).filter((peer) => peer?.uid);
54778
+ for (const peer of blockedPeers)
54779
+ discoveredPeerUids.add(peer.uid);
54201
54780
  blockedPeersReady = true;
54202
- rebuild({ projection: true });
54781
+ rebuild({ base: true });
54203
54782
  return blockedPeers;
54204
54783
  }
54205
54784
  return blockedPeers;
54206
54785
  }
54207
- function dropPeer(peer) {
54208
- const uid = peerUid(peer);
54209
- if (!uid)
54210
- return;
54211
- const discovered = discoveredPeerUids.has(uid);
54212
- if (!hiddenPeerUidSet.has(uid)) {
54213
- hiddenPeerUidSet.add(uid);
54214
- }
54215
- if (discovered) {
54216
- discoveredPeerUids.delete(uid);
54217
- }
54218
- if (typeof peer === "object" && peer?.uid) {
54219
- blockedPeers = [...blockedPeers.filter((item) => item?.uid !== uid), peer];
54220
- }
54221
- rebuild(discovered ? { base: true } : { projection: true });
54222
- }
54223
- function restorePeer(peer) {
54224
- const uid = peerUid(peer);
54225
- if (!uid)
54226
- return;
54227
- hiddenPeerUidSet.delete(uid);
54228
- blockedPeers = blockedPeers.filter((item) => item?.uid !== uid);
54229
- discoveredPeerUids.add(uid);
54230
- rebuild({ base: true });
54231
- }
54232
54786
  snapshot = makeSnapshot();
54233
54787
  return {
54234
54788
  getSnapshot() {
@@ -56868,7 +57422,7 @@ function compareRecentTransfer(a, b) {
56868
57422
  return String(b?.id || "").localeCompare(String(a?.id || ""));
56869
57423
  }
56870
57424
  function sameTransfer(left, right) {
56871
- 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;
57425
+ 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;
56872
57426
  }
56873
57427
  function sameTransfers(a = [], b = []) {
56874
57428
  if (a === b) {
@@ -56926,7 +57480,7 @@ function compactTransfer(tx, { incoming = false, bitcoinAddress = null, bitcoinT
56926
57480
  senderIdentityPublicKey: hexKey(tx.senderIdentityPublicKey),
56927
57481
  receiverIdentityPublicKey: hexKey(tx.receiverIdentityPublicKey),
56928
57482
  status: enumName(tx.status, TRANSFER_STATUS_BY_CODE),
56929
- totalValue: satValue(tx.totalValue),
57483
+ ...type === "TOKEN_TRANSFER" ? { tokenIdentifier: tx.tokenIdentifier, amountUnits: tx.amountUnits, network: tx.network } : { totalValue: satValue(tx.totalValue) },
56930
57484
  createdTime: tx.createdTime,
56931
57485
  updatedTime: tx.updatedTime,
56932
57486
  type,
@@ -57160,32 +57714,6 @@ function mergeTransferPageForWallet(currentTransfers = [], pageTransfers = [], w
57160
57714
  const page = compactTransfersForWallet(pageTransfers, walletPK);
57161
57715
  return mergeCompactedTransferPage(current, page, position);
57162
57716
  }
57163
- function mergeRecentSnapshotForWallet(currentTransfers = [], latestTransfers = [], walletPK) {
57164
- const currentById = new Map(currentTransfers.map((tx) => [String(tx?.id || ""), tx]));
57165
- const latest = mergeTransferPageForWallet([], latestTransfers, walletPK, "append").map((tx) => mergeTransferDetails(currentById.get(String(tx?.id || "")), tx));
57166
- if (!latest.length) {
57167
- return filterTransfersForWallet(currentTransfers, walletPK);
57168
- }
57169
- const latestIds = new Set(latest.map((tx) => String(tx.id)));
57170
- const latestById = new Map(latest.map((tx) => [String(tx.id), tx]));
57171
- const oldestLatestMs = getOldestTransferMs(latest);
57172
- const olderCurrent = filterTransfersForWallet(currentTransfers, walletPK).filter((tx) => {
57173
- const id = String(tx?.id || "");
57174
- if (latestIds.has(id)) {
57175
- const latestTx = latestById.get(id);
57176
- if (isPendingTransfer(latestTx) && !isPendingTransfer(tx)) {
57177
- return true;
57178
- }
57179
- return false;
57180
- }
57181
- if (isPendingTransfer(tx)) {
57182
- return false;
57183
- }
57184
- const createdMs = txCreatedMs(tx);
57185
- return oldestLatestMs == null || createdMs > 0 && createdMs < oldestLatestMs;
57186
- });
57187
- return mergeTransferPage(latest, olderCurrent, "append");
57188
- }
57189
57717
  function markClaimedTransfersCompleted(transfers = [], ids = []) {
57190
57718
  const claimedIds = new Set((Array.isArray(ids) ? ids : [ids]).map((id) => String(id || "")).filter(Boolean));
57191
57719
  if (!claimedIds.size)
@@ -57404,7 +57932,7 @@ async function refreshRequestedTransfers(owner, ids = []) {
57404
57932
  const resolvedIds = requestedTransfers.filter((transfer) => !isPendingTransfer(transfer)).map((transfer) => String(transfer.id));
57405
57933
  forgetPending(owner, resolvedIds);
57406
57934
  if (nextTransfers !== currentTransfers) {
57407
- await owner.snapshot.commitConfirmed(requestedTransfers.filter((transfer) => !isPendingTransfer(transfer)), { position: "prepend" });
57935
+ await owner.snapshot.commitHistory(overlayPending(owner, updates.map((transfer) => compactTransfer(transfer))), { position: "prepend" });
57408
57936
  if (!isCurrentGeneration(owner, generation))
57409
57937
  return [];
57410
57938
  owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), updates, owner.walletPK, "append"));
@@ -57522,8 +58050,7 @@ async function commitPendingReconciliation(owner, plan, result, generation) {
57522
58050
  const nextTransfers = result.updates.length ? overlayPending(owner, mergeTransferPageForWallet(current, result.updates, owner.walletPK, "append")) : current;
57523
58051
  forgetPending(owner, result.resolvedIds);
57524
58052
  if (nextTransfers !== current) {
57525
- const selectedIds = new Set(plan.selectedIds);
57526
- await owner.snapshot.commitConfirmed(nextTransfers.filter((transfer) => selectedIds.has(String(transfer?.id)) && !isPendingTransfer(transfer)), { position: "prepend" });
58053
+ await owner.snapshot.commitHistory(overlayPending(owner, result.updates.map((transfer) => compactTransfer(transfer))), { position: "prepend" });
57527
58054
  if (!isCurrentGeneration(owner, generation))
57528
58055
  return null;
57529
58056
  owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), result.updates, owner.walletPK, "append"));
@@ -57725,13 +58252,17 @@ function hydrateRecentHistory(owner) {
57725
58252
  return owner.hydratePromise;
57726
58253
  const generation = owner.generation;
57727
58254
  const startedAt = Date.now();
57728
- const request = owner.historyStore.query({ limit: WORKING_CONFIRMED_TRANSFER_LIMIT }).then((result) => {
57729
- if (!isCurrentGeneration2(owner, generation) || owner.snapshot.getTransfers().length || !result.transfers.length)
58255
+ const request = owner.historyStore.query({ limit: WORKING_CONFIRMED_TRANSFER_LIMIT }).then(async (result) => {
58256
+ const loadedIds = new Set(result.transfers.map((tx) => String(tx.id)));
58257
+ const missingPending = Object.keys(owner.historyStore.getSnapshot().projection?.pending || {}).filter((id) => !loadedIds.has(id));
58258
+ const pending = await Promise.all(missingPending.map((id) => owner.historyStore.getById(id)));
58259
+ const transfers = pending.length ? mergeTransferPage(result.transfers, pending.filter(Boolean), "prepend") : result.transfers;
58260
+ if (!isCurrentGeneration2(owner, generation) || owner.snapshot.getTransfers().length || !transfers.length)
57730
58261
  return false;
57731
58262
  owner.nextOffset = owner.historyStore.getSnapshot().nextOffset;
57732
- owner.snapshot.replace(result.transfers);
58263
+ owner.snapshot.replace(transfers);
57733
58264
  owner.snapshot.setReady();
57734
- markDone(owner.diag, "wallet.history.hydrate", startedAt, { count: result.transfers.length });
58265
+ markDone(owner.diag, "wallet.history.hydrate", startedAt, { count: transfers.length, pending: pendingTransfers(transfers).length });
57735
58266
  return true;
57736
58267
  }).catch((error) => {
57737
58268
  if (isCurrentGeneration2(owner, generation))
@@ -57790,13 +58321,12 @@ async function fetchHistoryPages(owner, options, generation) {
57790
58321
  completeOnStop: !!completeOnStop
57791
58322
  });
57792
58323
  try {
57793
- let stagedTransfers = owner.snapshot.getTransfers();
57794
58324
  let oldestFetchedMs = null;
57795
58325
  let stoppedAtKnownTransfer = false;
57796
58326
  while ((!owner.historyStore.getSnapshot().complete || force) && pages < maxPages) {
57797
58327
  if (!isCurrentGeneration2(owner, generation) || owner.backgroundSignal?.aborted === true)
57798
58328
  break;
57799
- if (!allHistory && Number.isFinite(sinceMs) && stagedHistoryCoverage(owner, stagedTransfers, oldestFetchedMs, sinceMs, force))
58329
+ if (!allHistory && Number.isFinite(sinceMs) && stagedHistoryCoverage(owner, owner.snapshot.getTransfers(), oldestFetchedMs, sinceMs, force))
57800
58330
  break;
57801
58331
  const offset = owner.nextOffset;
57802
58332
  const rawPage = await owner.readPage(TRANSFER_PAGE_LIMIT, offset, undefined, new Date(boundaryMs));
@@ -57807,12 +58337,11 @@ async function fetchHistoryPages(owner, options, generation) {
57807
58337
  const pageHitStopId = stopIds.size > 0 && page.transfers.some((transfer) => stopIds.has(String(transfer?.id)));
57808
58338
  if (page.transfers.length) {
57809
58339
  owner.pending.forgetResolvedFetched(page.transfers);
57810
- stagedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(stagedTransfers, page.transfers, owner.walletPK, "append"));
57811
58340
  const oldestPageMs = getOldestTransferMs(page.transfers);
57812
58341
  if (oldestPageMs != null)
57813
58342
  oldestFetchedMs = oldestFetchedMs == null ? oldestPageMs : Math.min(oldestFetchedMs, oldestPageMs);
57814
58343
  }
57815
- await owner.commitConfirmed(page.transfers, {
58344
+ await owner.commitHistory(page.transfers, {
57816
58345
  position: "append",
57817
58346
  nextOffset: page.nextOffset,
57818
58347
  boundaryMs,
@@ -57820,6 +58349,7 @@ async function fetchHistoryPages(owner, options, generation) {
57820
58349
  });
57821
58350
  if (!isCurrentGeneration2(owner, generation))
57822
58351
  break;
58352
+ owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), page.transfers, owner.walletPK, "append"));
57823
58353
  owner.nextOffset = Math.max(owner.nextOffset, page.nextOffset);
57824
58354
  await yieldToUi();
57825
58355
  if (pageHitStopId) {
@@ -57830,7 +58360,6 @@ async function fetchHistoryPages(owner, options, generation) {
57830
58360
  break;
57831
58361
  }
57832
58362
  if (isCurrentGeneration2(owner, generation)) {
57833
- owner.snapshot.replace(stagedTransfers);
57834
58363
  markDone(owner.diag, label, startedAt, {
57835
58364
  pages,
57836
58365
  loaded: owner.snapshot.getTransfers().length,
@@ -57927,13 +58456,12 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
57927
58456
  deltaAfterMs = Math.max(0, deltaState.newestMs - 1);
57928
58457
  deltaBeforeMs = Date.now();
57929
58458
  offset = 0;
57930
- await owner.commitConfirmed([], { deltaAfterMs, deltaBeforeMs, deltaNextOffset: 0 });
58459
+ await owner.commitHistory([], { deltaAfterMs, deltaBeforeMs, deltaNextOffset: 0 });
57931
58460
  if (!isCurrentGeneration2(owner, generation))
57932
58461
  return null;
57933
58462
  }
57934
58463
  const createdAfter = new Date(deltaAfterMs);
57935
58464
  const createdBefore = new Date(deltaBeforeMs);
57936
- let stagedTransfers = owner.snapshot.getTransfers();
57937
58465
  let rawCount = 0;
57938
58466
  let pages = 0;
57939
58467
  let complete = false;
@@ -57945,10 +58473,7 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
57945
58473
  rawCount += page.transfers.length;
57946
58474
  pages += 1;
57947
58475
  complete = !page.hasMore;
57948
- if (page.transfers.length) {
57949
- stagedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(stagedTransfers, page.transfers, walletKey, "prepend"));
57950
- }
57951
- await owner.commitConfirmed(page.transfers, {
58476
+ await owner.commitHistory(page.transfers, {
57952
58477
  position: "prepend",
57953
58478
  deltaAfterMs: complete ? null : deltaAfterMs,
57954
58479
  deltaBeforeMs: complete ? null : deltaBeforeMs,
@@ -57956,6 +58481,8 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
57956
58481
  });
57957
58482
  if (!isCurrentGeneration2(owner, generation))
57958
58483
  return null;
58484
+ owner.pending.forgetResolvedFetched(page.transfers);
58485
+ owner.snapshot.replace(mergeTransferPageForWallet(owner.snapshot.getTransfers(), page.transfers, walletKey, "prepend"));
57959
58486
  offset = page.nextOffset;
57960
58487
  if (complete)
57961
58488
  break;
@@ -57964,10 +58491,14 @@ async function refreshCachedHistory(owner, includePending = true, reason = "cach
57964
58491
  if (!isCurrentGeneration2(owner, generation) || owner.backgroundSignal?.aborted === true)
57965
58492
  return null;
57966
58493
  const pending = includePending ? await owner.pending.query() : [];
58494
+ if (!isCurrentGeneration2(owner, generation))
58495
+ return null;
58496
+ if (pending.length)
58497
+ await owner.commitHistory(pending, { position: "prepend" });
57967
58498
  if (!isCurrentGeneration2(owner, generation))
57968
58499
  return null;
57969
58500
  owner.pending.remember(pending.map((transfer) => transfer.id));
57970
- const nextTransfers = pending.length ? mergeTransferPageForWallet(stagedTransfers, pending, walletKey, "prepend") : stagedTransfers;
58501
+ const nextTransfers = pending.length ? mergeTransferPageForWallet(owner.snapshot.getTransfers(), pending, walletKey, "prepend") : owner.snapshot.getTransfers();
57971
58502
  owner.snapshot.replace(nextTransfers);
57972
58503
  const visibleCount = Math.min(nextTransfers.length, Math.max(owner.snapshot.getTransfers().length, RECENT_TRANSFER_LIMIT));
57973
58504
  owner.snapshot.publish(nextTransfers.slice(0, visibleCount));
@@ -58056,7 +58587,7 @@ async function loadRecentTransfers(owner, options = {}, generation = owner.gener
58056
58587
  const loadedPendingIncoming = includePending ? owner.pending.claimCandidates(latestTransfers) : [];
58057
58588
  const latestIds = new Set(latestTransfers.map((transfer) => String(transfer?.id)).filter(Boolean));
58058
58589
  const reconcileIds = previousHistoryComplete ? [] : previousTransfers.filter((transfer) => transfer?.id && !latestIds.has(String(transfer.id)) && !isPendingTransfer(transfer)).map((transfer) => String(transfer.id));
58059
- await owner.commitConfirmed(page.transfers, {
58590
+ await owner.commitHistory([...pending, ...page.transfers], {
58060
58591
  position: "prepend",
58061
58592
  nextOffset: Math.max(owner.historyStore.getSnapshot().nextOffset, page.nextOffset),
58062
58593
  boundaryMs,
@@ -58064,12 +58595,11 @@ async function loadRecentTransfers(owner, options = {}, generation = owner.gener
58064
58595
  });
58065
58596
  if (!isCurrentGeneration2(owner, generation))
58066
58597
  return null;
58067
- const fetchedTransfers = page.hasMore ? mergeRecentSnapshotForWallet(previousTransfers, latestTransfers, owner.walletPK) : mergeTransferPageForWallet([], latestTransfers, owner.walletPK);
58068
- const committedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(owner.snapshot.getTransfers(), fetchedTransfers, owner.walletPK, "append"));
58069
58598
  if (!showLoading)
58070
58599
  await yieldToUi();
58071
58600
  if (!isCurrentGeneration2(owner, generation))
58072
58601
  return null;
58602
+ const committedTransfers = boundedWorkingTransfers(mergeTransferPageForWallet(owner.snapshot.getTransfers(), [...pending, ...page.transfers], owner.walletPK, "append"));
58073
58603
  owner.nextOffset = Math.max(owner.nextOffset, page.nextOffset);
58074
58604
  const committedHistory = owner.historyStore.getSnapshot();
58075
58605
  const visibleCount = Math.min(committedTransfers.length, Math.max(owner.snapshot.getTransfers().length, RECENT_TRANSFER_LIMIT));
@@ -58119,8 +58649,8 @@ function getRecentTransfers(owner, options = {}) {
58119
58649
  owner.recentPromise = request;
58120
58650
  return request;
58121
58651
  }
58122
- function createTransferHistorySync({ walletPK, historyStore, initialOffset = 0, snapshot, readPage, commitConfirmed, pending, waitForWork, backgroundSignal = null, isClosed, diag = null } = {}) {
58123
- if (!walletPK || !historyStore || !snapshot || typeof readPage !== "function" || typeof commitConfirmed !== "function" || !pending || typeof waitForWork !== "function" || typeof isClosed !== "function") {
58652
+ function createTransferHistorySync({ walletPK, historyStore, initialOffset = 0, snapshot, readPage, commitHistory, pending, waitForWork, backgroundSignal = null, isClosed, diag = null } = {}) {
58653
+ if (!walletPK || !historyStore || !snapshot || typeof readPage !== "function" || typeof commitHistory !== "function" || !pending || typeof waitForWork !== "function" || typeof isClosed !== "function") {
58124
58654
  throw new Error("transfer history sync requires history, snapshot, pending, and read ports");
58125
58655
  }
58126
58656
  const owner = {
@@ -58128,7 +58658,7 @@ function createTransferHistorySync({ walletPK, historyStore, initialOffset = 0,
58128
58658
  historyStore,
58129
58659
  snapshot,
58130
58660
  readPage,
58131
- commitConfirmed,
58661
+ commitHistory,
58132
58662
  pending,
58133
58663
  waitForWork,
58134
58664
  backgroundSignal,
@@ -58185,6 +58715,7 @@ var ID_INDEX_PREFIX = "whi";
58185
58715
  var PEER_INDEX_PREFIX = "whp";
58186
58716
  var REPOSITORY_VERSION = 2;
58187
58717
  var SEGMENT_SIZE = 100;
58718
+ var WALLET_HISTORY_HEAD_SIZE = SEGMENT_SIZE;
58188
58719
  var SEGMENT_CACHE_LIMIT = 5;
58189
58720
  var ID_BUCKET_COUNT = 4096;
58190
58721
  var PEER_BUCKET_COUNT = 256;
@@ -58222,13 +58753,20 @@ function compareTransfers(left, right) {
58222
58753
  const delta = txCreatedMs(right) - txCreatedMs(left);
58223
58754
  return delta || transferId(right).localeCompare(transferId(left));
58224
58755
  }
58756
+ function mergeHistoryTransfer(current, next) {
58757
+ if (!current)
58758
+ return next;
58759
+ if (!isPendingTransfer(current) && isPendingTransfer(next) || txUpdatedMs(next) < txUpdatedMs(current))
58760
+ return current;
58761
+ return { ...current, ...next };
58762
+ }
58225
58763
  function normalizeTransfers(transfers, walletPK) {
58226
58764
  const byId = new Map;
58227
58765
  for (const transfer of Array.isArray(transfers) ? transfers : EMPTY_TRANSFERS) {
58228
58766
  const id = transferId(transfer);
58229
58767
  if (!id || !isVisibleTransfer(transfer) || !transferBelongsToWallet(transfer, walletPK))
58230
58768
  continue;
58231
- byId.set(id, jsonClone(transfer));
58769
+ byId.set(id, mergeHistoryTransfer(byId.get(id), jsonClone(transfer)));
58232
58770
  }
58233
58771
  return [...byId.values()].sort(compareTransfers);
58234
58772
  }
@@ -58253,7 +58791,8 @@ function emptyProjection() {
58253
58791
  days: {},
58254
58792
  hours: {},
58255
58793
  months: {},
58256
- peers: {}
58794
+ peers: {},
58795
+ pending: {}
58257
58796
  };
58258
58797
  }
58259
58798
  function cleanStats(value) {
@@ -58268,6 +58807,7 @@ function cleanStats(value) {
58268
58807
  }
58269
58808
  function cleanProjection(value) {
58270
58809
  const projection = emptyProjection();
58810
+ projection.pending = { ...value?.pending };
58271
58811
  projection.all = cleanStats(value?.all);
58272
58812
  for (const [key, stats] of Object.entries(value?.days || {}))
58273
58813
  projection.days[key] = cleanStats(stats);
@@ -58304,6 +58844,8 @@ function applyStats(stats, transfer, sign2, { excludeFunding = false } = {}) {
58304
58844
  const amount = Number(transfer?.totalValue) || 0;
58305
58845
  const incoming = isIncomingTransfer(transfer);
58306
58846
  stats.count += sign2;
58847
+ if (incoming && isPendingTransfer(transfer))
58848
+ return;
58307
58849
  stats.vol += sign2 * amount;
58308
58850
  if (incoming) {
58309
58851
  stats.received += sign2 * amount;
@@ -58322,6 +58864,11 @@ function normalizeStats(stats) {
58322
58864
  return stats;
58323
58865
  }
58324
58866
  function applyProjection(projection, transfer, sign2) {
58867
+ const id = transferId(transfer);
58868
+ if (sign2 > 0 && isPendingTransfer(transfer))
58869
+ projection.pending[id] = txCreatedMs(transfer);
58870
+ else
58871
+ delete projection.pending[id];
58325
58872
  const { day, hour, month } = timeKeys(transfer);
58326
58873
  projection.days[day] ||= emptyStats();
58327
58874
  projection.hours[hour] ||= emptyStats();
@@ -58407,11 +58954,12 @@ function normalizeManifest(value, walletPK) {
58407
58954
  projection: cleanProjection(value.projection)
58408
58955
  };
58409
58956
  }
58410
- function repositorySnapshot(manifest, ready, syncing = false) {
58957
+ function repositorySnapshot(manifest, ready, syncing = false, head = EMPTY_TRANSFERS) {
58411
58958
  return Object.freeze({
58412
58959
  walletPK: manifest.walletPK,
58413
58960
  ready,
58414
58961
  syncing,
58962
+ head,
58415
58963
  count: manifest.count,
58416
58964
  complete: manifest.complete,
58417
58965
  bitcoinOldestMs: manifest.bitcoinOldestMs,
@@ -58459,8 +59007,14 @@ function buildSegmentCommitPlan({ manifest, incoming, removedIds, mappings, oldS
58459
59007
  const incomingById = new Map(incoming.map((transfer) => [transferId(transfer), transfer]));
58460
59008
  const working = new Map;
58461
59009
  for (const [segmentId, segmentTransfers] of oldSegments) {
58462
- for (const transfer of segmentTransfers)
58463
- previousById.set(transferId(transfer), transfer);
59010
+ for (const transfer of segmentTransfers) {
59011
+ const id = transferId(transfer);
59012
+ previousById.set(id, transfer);
59013
+ const next2 = incomingById.get(id);
59014
+ if (!next2)
59015
+ continue;
59016
+ incomingById.set(id, mergeHistoryTransfer(transfer, next2));
59017
+ }
58464
59018
  const next = segmentTransfers.filter((transfer) => !removedIds.has(transferId(transfer))).map((transfer) => incomingById.get(transferId(transfer)) || transfer);
58465
59019
  working.set(segmentId, normalizeTransfers(next, walletPK));
58466
59020
  }
@@ -58676,6 +59230,15 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
58676
59230
  }
58677
59231
  return rememberSegment(id, transfers);
58678
59232
  };
59233
+ const readHead = async () => {
59234
+ const transfers = [];
59235
+ for (const segment of manifest.segments) {
59236
+ transfers.push(...await readSegment(segment.id));
59237
+ if (transfers.length >= WALLET_HISTORY_HEAD_SIZE)
59238
+ break;
59239
+ }
59240
+ return transfers.length ? Object.freeze(transfers.slice(0, WALLET_HISTORY_HEAD_SIZE)) : EMPTY_TRANSFERS;
59241
+ };
58679
59242
  const readIdMappings = async (ids, generation = manifest.generation) => {
58680
59243
  const grouped = new Map;
58681
59244
  for (const id of ids) {
@@ -58720,7 +59283,7 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
58720
59283
  if (!wrote)
58721
59284
  throw new Error("wallet history checkpoint write failed");
58722
59285
  manifest = normalizeManifest(nextManifest, requestedWalletPK);
58723
- snapshot = repositorySnapshot(manifest, true);
59286
+ snapshot = repositorySnapshot(manifest, true, false, await readHead());
58724
59287
  emit(listeners);
58725
59288
  };
58726
59289
  const pruneSegments = async () => {
@@ -58738,6 +59301,13 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
58738
59301
  };
58739
59302
  const prunePromise = pruneSegments();
58740
59303
  await prunePromise;
59304
+ const initialHead = await readHead().catch(async (error) => {
59305
+ if (!recoveryPromise)
59306
+ throw error;
59307
+ await recoveryPromise;
59308
+ return EMPTY_TRANSFERS;
59309
+ });
59310
+ snapshot = repositorySnapshot(manifest, true, false, initialHead);
58741
59311
  const readCommitSource = async (incoming, removedIds, position) => {
58742
59312
  const ids = [...new Set([...incoming.map(transferId), ...removedIds])];
58743
59313
  const { mappings, known, buckets: idBuckets } = await readIdMappings(ids);
@@ -58809,7 +59379,9 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
58809
59379
  if (!wrote)
58810
59380
  throw new Error("wallet history checkpoint write failed");
58811
59381
  manifest = normalizeManifest(nextManifest, requestedWalletPK);
58812
- snapshot = repositorySnapshot(manifest, true);
59382
+ for (const record of plan.newSegmentRecords)
59383
+ rememberSegment(record.descriptor.id, record.transfers);
59384
+ snapshot = repositorySnapshot(manifest, true, false, await readHead());
58813
59385
  emit(listeners);
58814
59386
  } else {
58815
59387
  for (const record of plan.newSegmentRecords) {
@@ -58836,6 +59408,10 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
58836
59408
  await prunePromise;
58837
59409
  const incoming = normalizeTransfers(transfers, requestedWalletPK);
58838
59410
  const removedIds = new Set((Array.isArray(options.removedIds) ? options.removedIds : []).map(String).filter(Boolean));
59411
+ for (const transfer of transfers) {
59412
+ if (transfer?.id && transferBelongsToWallet(transfer, requestedWalletPK) && !isVisibleTransfer(transfer))
59413
+ removedIds.add(String(transfer.id));
59414
+ }
58839
59415
  const bitcoinTimes = incoming.filter((tx) => !tx.tokenIdentifier).map(txCreatedMs);
58840
59416
  if (bitcoinTimes.length)
58841
59417
  options = { ...options, bitcoinOldestMs: Math.min(manifest.bitcoinOldestMs ?? Infinity, ...bitcoinTimes) };
@@ -58869,13 +59445,13 @@ async function openWalletHistoryRepository({ localCache, walletPK } = {}) {
58869
59445
  commitPage(transfers, options = {}) {
58870
59446
  if (corruptionDetected)
58871
59447
  return Promise.reject(new Error("wallet history cache recovery in progress"));
58872
- snapshot = repositorySnapshot(manifest, true, true);
59448
+ snapshot = repositorySnapshot(manifest, true, true, snapshot.head);
58873
59449
  emit(listeners);
58874
59450
  const run = () => commit(transfers, options);
58875
59451
  const result = writeTail.then(run, run);
58876
59452
  writeTail = result.catch(() => {}).finally(() => {
58877
59453
  if (!closed) {
58878
- snapshot = repositorySnapshot(manifest, true, false);
59454
+ snapshot = repositorySnapshot(manifest, true, false, snapshot.head);
58879
59455
  emit(listeners);
58880
59456
  }
58881
59457
  });
@@ -59174,9 +59750,9 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59174
59750
  const snapshot = transferStore.getSnapshot();
59175
59751
  return lowerText(snapshot.walletPK) === walletKey && snapshot.ready === true;
59176
59752
  };
59177
- const commitConfirmedHistoryPage = (pageTransfers, options = {}) => {
59178
- const confirmed = compactTransfersForWallet(pageTransfers, walletKey).filter((transfer) => !isPendingTransfer(transfer));
59179
- return historyStore.commitPage(confirmed, options);
59753
+ const commitHistoryPage = (pageTransfers, options = {}) => {
59754
+ const updates = pageTransfers.map((transfer) => compactTransfer(transfer)).filter((transfer) => transfer && transferBelongsToWallet(transfer, walletKey));
59755
+ return historyStore.commitPage(updates, options);
59180
59756
  };
59181
59757
  const forgetPendingLookupIds = (ids = []) => pendingReconciliation.forget(ids);
59182
59758
  const rememberPendingTransferIds = (ids = []) => pendingReconciliation.remember(ids);
@@ -59212,9 +59788,6 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59212
59788
  if (closed)
59213
59789
  return current;
59214
59790
  let ownedTransfers = owned ? nextTransfers : filterTransfersForWallet(nextTransfers, walletKey);
59215
- if (current.length && ownedTransfers.length && ownedTransfers.length < current.length) {
59216
- ownedTransfers = mergeTransferPageForWallet(ownedTransfers, current, walletKey, "append");
59217
- }
59218
59791
  ownedTransfers = boundedWorkingTransfers(applyTransferStatusOverlays(ownedTransfers));
59219
59792
  const committed = sameTransfers(current, ownedTransfers) ? current : ownedTransfers;
59220
59793
  transferStore.setHistory({ transfers: committed, walletPK: walletKey });
@@ -59351,7 +59924,7 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59351
59924
  return [];
59352
59925
  const nextTransfers = markClaimedTransfersCompleted(current, knownIds);
59353
59926
  if (nextTransfers !== current) {
59354
- commitConfirmedHistoryPage(nextTransfers.filter((transfer) => knownIds.includes(String(transfer?.id))), { position: "prepend" });
59927
+ commitHistoryPage(nextTransfers.filter((transfer) => knownIds.includes(String(transfer?.id))), { position: "prepend" });
59355
59928
  commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
59356
59929
  setTxReadyValue(true);
59357
59930
  }
@@ -59376,7 +59949,7 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59376
59949
  snapshot: {
59377
59950
  getTransfers: currentTransfers,
59378
59951
  replace: (transfers) => commitTransferSnapshot(transfers, { publish: true, owned: true }),
59379
- commitConfirmed: commitConfirmedHistoryPage,
59952
+ commitHistory: commitHistoryPage,
59380
59953
  setReady: () => setTxReadyValue(true)
59381
59954
  },
59382
59955
  reads: {
@@ -59400,7 +59973,7 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59400
59973
  setLoading: setIsTxLoading
59401
59974
  },
59402
59975
  readPage: fetchTransfersPage,
59403
- commitConfirmed: commitConfirmedHistoryPage,
59976
+ commitHistory: commitHistoryPage,
59404
59977
  pending: {
59405
59978
  query: getPendingTransfersSnapshot,
59406
59979
  isQuerying: () => !!pendingQueryPromiseRef.current,
@@ -59457,7 +60030,10 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59457
60030
  const current = currentTransfers();
59458
60031
  const nextTransfers = mergeTransferPageForWallet(current, incomingTransfers, walletKey, "prepend");
59459
60032
  if (nextTransfers !== current) {
59460
- commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
60033
+ await commitHistoryPage(incomingTransfers, { position: "prepend" });
60034
+ if (closed)
60035
+ return [];
60036
+ commitTransferSnapshot(mergeTransferPageForWallet(currentTransfers(), incomingTransfers, walletKey, "prepend"), { publish: true, owned: true });
59461
60037
  setTxReadyValue(true);
59462
60038
  }
59463
60039
  }
@@ -59499,10 +60075,8 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59499
60075
  forgetPendingLookupIds(resolvedUpdateIds);
59500
60076
  }
59501
60077
  }
59502
- const current = currentTransfers();
59503
- const nextTransfers = mergeTransferPageForWallet(current, updates, walletKey, "prepend");
59504
- await commitConfirmedHistoryPage(updates, { position: "prepend" });
59505
- commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
60078
+ await commitHistoryPage(updates, { position: "prepend" });
60079
+ commitTransferSnapshot(mergeTransferPageForWallet(currentTransfers(), updates, walletKey, "prepend"), { publish: true, owned: true });
59506
60080
  setTxReadyValue(true);
59507
60081
  markDone(diag, "wallet.claimedTxs", startedAt2, { found: true, count: updates.length });
59508
60082
  return updates;
@@ -59527,10 +60101,8 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
59527
60101
  forgetPendingLookupIds([compact.id]);
59528
60102
  }
59529
60103
  }
59530
- const current = currentTransfers();
59531
- const nextTransfers = mergeTransferPageForWallet(current, [compact], walletKey, "prepend");
59532
- await commitConfirmedHistoryPage([compact], { position: "prepend" });
59533
- commitTransferSnapshot(nextTransfers, { publish: true, owned: true });
60104
+ await commitHistoryPage([compact], { position: "prepend" });
60105
+ commitTransferSnapshot(mergeTransferPageForWallet(currentTransfers(), [compact], walletKey, "prepend"), { publish: true, owned: true });
59534
60106
  setTxReadyValue(true);
59535
60107
  markDone(diag, "wallet.claimedTx", startedAt, { found: true });
59536
60108
  return compact;
@@ -60337,27 +60909,21 @@ function tokenHistoryBalance(tokens, unitsByToken = {}) {
60337
60909
  }
60338
60910
 
60339
60911
  // ../../core/wallet/txlist.js
60340
- async function readTxListPage(pending, queryHistory, { offset = 0, limit = 100 } = {}) {
60912
+ async function readTxListPage(queryHistory, { offset = 0, limit = 100 } = {}) {
60341
60913
  const start2 = Math.max(0, Math.floor(offset));
60342
60914
  const count = Math.max(1, Math.min(500, Math.floor(limit)));
60343
- const head = pending.slice(start2, start2 + count);
60344
- const remaining = count - head.length;
60345
- const history = remaining ? await queryHistory({ offset: Math.max(0, start2 - pending.length), limit: remaining }) : { transfers: [] };
60346
- return { offset: start2, transfers: [...head, ...history.transfers] };
60347
- }
60348
- async function findTxListPeriod(pending, history, queryHistory, { sinceMs, untilMs, hourly }) {
60349
- const pendingIndex = pending.findIndex((tx) => tx.createdMs >= sinceMs && tx.createdMs < untilMs);
60350
- if (pendingIndex >= 0)
60351
- return pendingIndex;
60915
+ const history = await queryHistory({ offset: start2, limit: count });
60916
+ return { offset: start2, transfers: history.transfers };
60917
+ }
60918
+ async function findTxListPeriod(history, queryHistory, { sinceMs, hourly }) {
60352
60919
  const key = (hourly ? dayHourKey : dayKey)(new Date(sinceMs));
60353
60920
  const buckets = history.projection[hourly ? "hours" : "days"];
60354
60921
  const newer = Object.entries(buckets).reduce((count, [period, stats]) => count + (period > key ? stats.count : 0), 0);
60355
60922
  if (buckets[key]?.count)
60356
- return pending.length + newer;
60923
+ return newer;
60357
60924
  const offset = Math.max(0, newer - 1);
60358
60925
  const { transfers } = await queryHistory({ offset, limit: 2 });
60359
- const candidates = pending.map((tx, index) => ({ tx, index }));
60360
- transfers.forEach((tx, index) => candidates.push({ tx, index: pending.length + offset + index }));
60926
+ const candidates = transfers.map((tx, index) => ({ tx, index: offset + index }));
60361
60927
  candidates.sort((a, b) => Math.abs(a.tx.createdMs - sinceMs) - Math.abs(b.tx.createdMs - sinceMs));
60362
60928
  return candidates[0]?.index ?? -1;
60363
60929
  }
@@ -60596,14 +61162,14 @@ function mergeTxSearchResults(loaded, found, limit = TX_SEARCH_DEFAULT_LIMIT) {
60596
61162
  }
60597
61163
  return [...byId.values()].sort(byRecentTx).slice(0, max2);
60598
61164
  }
60599
- function getDailySeries({ days, transactions, aggregatedData, balance, historyComplete, oldestMs, cache }) {
61165
+ function getDailySeries({ days, transactions, aggregatedData, balance, historyComplete, oldestMs, cache, nowMs: nowMs2 = Date.now() }) {
60600
61166
  if (!transactions.length || balance == null)
60601
61167
  return [];
60602
61168
  const cacheKey = `daily-${days}`;
60603
- if (cache.has(cacheKey)) {
60604
- return cache.get(cacheKey);
60605
- }
60606
- const today = new Date;
61169
+ const clock = Math.floor(nowMs2 / 60000);
61170
+ if (cache.get(cacheKey)?.clock === clock)
61171
+ return cache.get(cacheKey).series;
61172
+ const today = new Date(nowMs2);
60607
61173
  today.setHours(0, 0, 0, 0);
60608
61174
  const series = [];
60609
61175
  let runningBalance = Number(balance);
@@ -60614,84 +61180,44 @@ function getDailySeries({ days, transactions, aggregatedData, balance, historyCo
60614
61180
  const d = new Date(today);
60615
61181
  d.setDate(d.getDate() - i2);
60616
61182
  const key = dayKey(d);
60617
- series.push({ date: key, balance: runningBalance });
61183
+ const end = new Date(d);
61184
+ end.setDate(end.getDate() + 1);
61185
+ series.push({ date: key, timestamp: i2 === 0 ? nowMs2 : end.getTime(), now: i2 === 0, balance: runningBalance });
60618
61186
  const dayData = aggregatedData.dayMap.get(key);
60619
61187
  if (dayData) {
60620
61188
  runningBalance -= dayData.net;
60621
61189
  }
60622
61190
  }
60623
61191
  const result = series.reverse();
60624
- cache.set(cacheKey, result);
61192
+ cache.set(cacheKey, { clock, series: result });
60625
61193
  return result;
60626
61194
  }
60627
- function getHourlySeriesForData({ hours, prefix = "today", transactions, aggregatedData, balance, historyComplete, oldestMs, cache }) {
61195
+ function getHourlySeriesForData({ hours, prefix = "today", transactions, aggregatedData, balance, historyComplete, oldestMs, cache, nowMs: nowMs2 = Date.now() }) {
60628
61196
  if (!transactions.length || balance == null)
60629
61197
  return [];
60630
61198
  const cacheKey = `hourly-${hours}-${prefix}`;
60631
- if (cache.has(cacheKey)) {
60632
- return cache.get(cacheKey);
60633
- }
60634
- const now = new Date;
60635
- const series = [];
61199
+ const clock = Math.floor(nowMs2 / 60000);
61200
+ if (cache.get(cacheKey)?.clock === clock)
61201
+ return cache.get(cacheKey).series;
61202
+ const now = new Date(nowMs2);
61203
+ const series = [{ hour: "now", timestamp: nowMs2, now: true, balance: Number(balance) }];
60636
61204
  let runningBalance = Number(balance);
60637
- const today = dayKey(now);
60638
- const yesterday = dayKey(new Date(now.getTime() - DAY_MS));
61205
+ const today = new Date(now);
61206
+ today.setHours(0, 0, 0, 0);
60639
61207
  const coveredHours = historyComplete ? hours : coveredUnitsSince(oldestMs, HOUR_MS, now.getTime());
60640
61208
  const actualHours = Math.min(hours, coveredHours);
60641
- if (prefix === "today") {
60642
- const currentHour = now.getHours();
60643
- const maxHour = Math.min(actualHours - 1, currentHour);
60644
- for (let i2 = 0;i2 <= maxHour; i2++) {
60645
- const key = `${today}-${hourKey(i2)}`;
60646
- const hourData = aggregatedData.hourMap.get(key);
60647
- if (hourData) {
60648
- runningBalance -= hourData.net;
60649
- }
60650
- }
60651
- for (let i2 = 0;i2 <= maxHour; i2++) {
60652
- const hour = hourKey(i2);
60653
- series.push({ hour, balance: runningBalance });
60654
- const key = `${today}-${hour}`;
60655
- const hourData = aggregatedData.hourMap.get(key);
60656
- if (hourData) {
60657
- runningBalance += hourData.net;
60658
- }
60659
- }
60660
- series.push({ hour: "now", balance: Number(balance) });
60661
- } else if (prefix === "24h") {
60662
- const currentHour = now.getHours();
60663
- const firstOffset = Math.max(0, 24 - actualHours);
60664
- for (let i2 = 0;i2 <= currentHour; i2++) {
60665
- const key = `${today}-${hourKey(i2)}`;
60666
- const hourData = aggregatedData.hourMap.get(key);
60667
- if (hourData)
60668
- runningBalance -= hourData.net;
60669
- }
60670
- for (let i2 = Math.max(currentHour + 1, firstOffset);i2 < 24; i2++) {
60671
- const key = `${yesterday}-${hourKey(i2)}`;
60672
- const hourData = aggregatedData.hourMap.get(key);
60673
- if (hourData)
60674
- runningBalance -= hourData.net;
60675
- }
60676
- for (let i2 = Math.max(currentHour + 1, firstOffset);i2 < 24; i2++) {
60677
- const hour = hourKey(i2);
60678
- series.push({ hour, balance: runningBalance });
60679
- const key = `${yesterday}-${hour}`;
60680
- const hourData = aggregatedData.hourMap.get(key);
60681
- if (hourData)
60682
- runningBalance += hourData.net;
60683
- }
60684
- for (let i2 = firstOffset > currentHour ? firstOffset : 0;i2 <= currentHour; i2++) {
60685
- const hour = hourKey(i2);
60686
- series.push({ hour, balance: runningBalance });
60687
- const key = `${today}-${hour}`;
60688
- const hourData = aggregatedData.hourMap.get(key);
60689
- if (hourData)
60690
- runningBalance += hourData.net;
60691
- }
60692
- }
60693
- cache.set(cacheKey, series);
60694
- return series;
61209
+ const endHour = new Date(now);
61210
+ endHour.setMinutes(0, 0, 0);
61211
+ for (let offset = 0;offset < actualHours; offset++) {
61212
+ const date = new Date(endHour.getTime() - offset * HOUR_MS);
61213
+ if (prefix === "today" && date < today)
61214
+ break;
61215
+ runningBalance -= aggregatedData.hourMap.get(dayHourKey(date))?.net || 0;
61216
+ series.push({ hour: hourKey(date), timestamp: date.getTime(), balance: runningBalance });
61217
+ }
61218
+ const result = series.reverse();
61219
+ cache.set(cacheKey, { clock, series: result });
61220
+ return result;
60695
61221
  }
60696
61222
  function getTxsInRangeFrom(sortedTransactions, timeRange) {
60697
61223
  if (!sortedTransactions.length)
@@ -60899,11 +61425,8 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
60899
61425
  transfers: result.transfers.map((transfer) => getVisibleWalletTransfer(transfer, sources.walletPK)).filter(Boolean)
60900
61426
  };
60901
61427
  }
60902
- function pendingList() {
60903
- return readCurrentAggregatedData(sources.walletPK).sortedTxs.filter((tx) => tx.pending);
60904
- }
60905
61428
  function getTxListPage(options) {
60906
- return readTxListPage(pendingList(), queryHistory, options);
61429
+ return readTxListPage(queryHistory, options);
60907
61430
  }
60908
61431
  async function findTxListPeriod2(period) {
60909
61432
  const { sinceMs, untilMs } = period;
@@ -60921,7 +61444,7 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
60921
61444
  return -1;
60922
61445
  if (!covered())
60923
61446
  throw new Error("transaction history is unavailable for this date");
60924
- return findTxListPeriod(pendingList(), historyStore.getSnapshot(), queryHistory, period);
61447
+ return findTxListPeriod(historyStore.getSnapshot(), queryHistory, period);
60925
61448
  }
60926
61449
  async function getHistoryById(txId, tokenIdentifier) {
60927
61450
  const { walletPK, lookupTxById } = sources;
@@ -60941,31 +61464,17 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
60941
61464
  const peerKey = lowerText(peerPK);
60942
61465
  const limit = Math.max(1, Math.min(500, Math.floor(Number(options.limit) || 100)));
60943
61466
  const offset = Math.max(0, Math.floor(Number(options.offset) || 0));
60944
- const pendingData = getPeerDataFromTransfers(transferStore.getSnapshot().transfers.filter(isPendingTransfer), sources.walletPK, peerKey);
60945
- const pending = pendingData.txs;
60946
- const pendingPage = pending.slice(offset, offset + limit);
60947
- const historyOffset = Math.max(0, offset - pending.length);
60948
- const historyLimit = Math.max(0, limit - pendingPage.length);
60949
- const result = historyLimit ? await queryHistory({ ...options, peerPK: peerKey, offset: historyOffset, limit: historyLimit }) : { transfers: [], cursor: null };
61467
+ const result = await queryHistory({ ...options, peerPK: peerKey, offset, limit });
60950
61468
  const storedStats = historyStore.getSnapshot().projection?.peers?.[peerKey];
60951
- const confirmedStats = {
61469
+ const stats = {
60952
61470
  sent: Number(storedStats?.sent) || 0,
60953
61471
  received: Number(storedStats?.received) || 0,
60954
61472
  net: Number(storedStats?.net) || 0,
60955
61473
  vol: Number(storedStats?.vol) || 0,
60956
61474
  cnt: Number(storedStats?.count) || 0
60957
61475
  };
60958
- const pendingStats = pendingData.stats || { sent: 0, received: 0, net: 0, vol: 0, cnt: 0 };
60959
- const stats = confirmedStats.cnt || pendingStats.cnt ? {
60960
- sent: confirmedStats.sent + pendingStats.sent,
60961
- received: confirmedStats.received + pendingStats.received,
60962
- net: confirmedStats.net + pendingStats.net,
60963
- vol: confirmedStats.vol + pendingStats.vol,
60964
- cnt: confirmedStats.cnt + pendingStats.cnt
60965
- } : null;
60966
- const txs = [...pendingPage, ...result.transfers];
60967
- const total = confirmedStats.cnt + pending.length;
60968
- return { ...result, transfers: txs, txs, stats, total, hasMore: offset + txs.length < total };
61476
+ const txs = result.transfers;
61477
+ return { ...result, txs, stats: stats.cnt ? stats : null, total: stats.cnt, hasMore: offset + txs.length < stats.cnt };
60969
61478
  }
60970
61479
  async function getHistorySummary(timeRange = "all-time") {
60971
61480
  const result = await historyStore.summarize({ sinceMs: getTxRangeStartMs(timeRange) });
@@ -61021,31 +61530,16 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
61021
61530
  }
61022
61531
  return [...combined.values()].sort(byRecentTx).slice(0, limit);
61023
61532
  }
61024
- function addCashSeries(series, hourly, prefix) {
61025
- const history = historyStore.getSnapshot();
61026
- const tokens = sources.tokens;
61533
+ function addCashSeries(series, hourly, history, tokens) {
61027
61534
  const coverage = !tokens.length || history.tokenComplete;
61028
- const now = new Date;
61029
61535
  const buckets = history.projection[hourly ? "hours" : "days"];
61030
61536
  return series.map((point) => {
61031
- let timestamp;
61032
- if (hourly) {
61033
- timestamp = new Date(now);
61034
- if (point.hour !== "now") {
61035
- const hour = Number(point.hour.slice(0, 2));
61036
- timestamp.setHours(hour, 0, 0, 0);
61037
- if (prefix === "24h" && hour > now.getHours())
61038
- timestamp.setDate(timestamp.getDate() - 1);
61039
- }
61040
- } else {
61041
- timestamp = new Date(`${point.date}T00:00:00`);
61042
- timestamp.setDate(timestamp.getDate() + 1);
61043
- }
61044
- const known = coverage || history.tokenOldestMs != null && timestamp.getTime() > history.tokenOldestMs;
61537
+ const timestamp = new Date(point.timestamp);
61538
+ const known = point.now || coverage || history.tokenOldestMs != null && timestamp.getTime() > history.tokenOldestMs;
61045
61539
  const key = (hourly ? dayHourKey : dayKey)(timestamp);
61046
61540
  const deltas = {};
61047
- if (point.hour !== "now")
61048
- for (const [bucketKey, stats] of Object.entries(buckets)) {
61541
+ if (!point.now)
61542
+ for (const [bucketKey, stats] of Object.entries(buckets || {})) {
61049
61543
  if (bucketKey < key)
61050
61544
  continue;
61051
61545
  for (const [id, units] of Object.entries(stats.tokens || {}))
@@ -61055,14 +61549,14 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
61055
61549
  });
61056
61550
  }
61057
61551
  function buildSnapshot() {
61058
- const { balance, walletPK, oldestTxMs, txReady, isTxLoading, ensureTxCoverage, loadMoreTxs } = sources;
61552
+ const { balance, tokens, walletPK, oldestTxMs, txReady, isTxLoading, ensureTxCoverage, loadMoreTxs } = sources;
61059
61553
  const storeSnapshot = transferStore.getSnapshot();
61060
61554
  const historySnapshot = historyStore.getSnapshot();
61061
61555
  observedStoreSnapshot = storeSnapshot;
61062
61556
  observedHistorySnapshot = historySnapshot;
61063
61557
  const transfers = storeSnapshot.transfers;
61064
61558
  const aggregatedData = deferAggregation ? EMPTY_AGG : readCurrentAggregatedData(walletPK);
61065
- const projectedData = projectionData(historySnapshot);
61559
+ const projectedData = historySnapshot.walletPK === walletPK ? projectionData(historySnapshot) : null;
61066
61560
  const transactions = aggregatedData.enrichedTxs;
61067
61561
  const sortedTransactions = aggregatedData.sortedTxs;
61068
61562
  const readLiveTransfers = () => deferAggregation ? transferStore.getSnapshot().transfers : transfers;
@@ -61132,16 +61626,17 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
61132
61626
  cache: seriesCache.current
61133
61627
  });
61134
61628
  };
61135
- const getHistorySeries = (days) => addCashSeries(getDailySeries({
61629
+ const getHistorySeries = (days, nowMs2 = Date.now()) => !projectedData ? [] : addCashSeries(getDailySeries({
61136
61630
  days,
61137
61631
  transactions: projectedData.enrichedTxs,
61138
61632
  aggregatedData: projectedData,
61139
61633
  balance,
61140
61634
  historyComplete: historySnapshot.complete,
61141
61635
  oldestMs: historySnapshot.bitcoinOldestMs,
61142
- cache: historySeriesCache.current
61143
- }), false);
61144
- const getHistoryHourlySeries = (hours, prefix = "today") => addCashSeries(getHourlySeriesForData({
61636
+ cache: historySeriesCache.current,
61637
+ nowMs: nowMs2
61638
+ }), false, historySnapshot, tokens);
61639
+ const getHistoryHourlySeries = (hours, prefix = "today", nowMs2 = Date.now()) => !projectedData ? [] : addCashSeries(getHourlySeriesForData({
61145
61640
  hours,
61146
61641
  prefix,
61147
61642
  transactions: projectedData.enrichedTxs,
@@ -61149,8 +61644,9 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
61149
61644
  balance,
61150
61645
  historyComplete: historySnapshot.complete,
61151
61646
  oldestMs: historySnapshot.bitcoinOldestMs,
61152
- cache: historySeriesCache.current
61153
- }), true, prefix);
61647
+ cache: historySeriesCache.current,
61648
+ nowMs: nowMs2
61649
+ }), true, historySnapshot, tokens);
61154
61650
  const getTxById = (txId) => getVisibleTransferById(readLiveTransfers(), walletPK, txId, transferByIdRef);
61155
61651
  const getPeerTxs = (peerPK) => peerPK ? readPeerData(peerPK).txs : EMPTY_TXS2;
61156
61652
  const getPeerStats = (peerPK) => peerPK ? readPeerData(peerPK).stats : null;
@@ -61183,7 +61679,7 @@ function createTxData({ transferStore, historyStore, diag = null, deferAggregati
61183
61679
  queryHistory,
61184
61680
  getTxListPage,
61185
61681
  findTxListPeriod: findTxListPeriod2,
61186
- txListCount: historySnapshot.count + transfers.filter((tx) => isPendingTransfer(tx) && isVisibleTransfer(tx) && transferBelongsToWallet(tx, walletPK)).length,
61682
+ txListCount: historySnapshot.walletPK === walletPK ? historySnapshot.count : 0,
61187
61683
  getTxById,
61188
61684
  searchTxs,
61189
61685
  getPeerTxs,
@@ -61790,8 +62286,7 @@ function openAccount(options = {}) {
61790
62286
  getPeerProfile: getChatPeerProfile,
61791
62287
  refreshPeerProfile: refreshChatPeerProfile,
61792
62288
  resolvePeerProfile: resolveChatPeerProfile,
61793
- maintenance: messageMaintenance,
61794
- resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
62289
+ maintenance: messageMaintenance
61795
62290
  }, chatSources());
61796
62291
  const calls = createCallSession({
61797
62292
  cloud,
@@ -61818,11 +62313,11 @@ function openAccount(options = {}) {
61818
62313
  function chatSources(sessionOverride = chatSession) {
61819
62314
  const currentUser = state.user || {};
61820
62315
  const session = sessionOverride;
61821
- const cachedBlacklist = session?.localCache?.read?.()?.blacklist;
61822
62316
  return {
61823
62317
  uid: currentUser.uid || "",
61824
- blocked: currentUser.blockedReady ? currentUser.blocked : cachedBlacklist || [],
61825
- blockedReady: currentUser.blockedReady || Array.isArray(cachedBlacklist),
62318
+ blocked: currentUser.blocked,
62319
+ blockedPending: currentUser.blockedPending,
62320
+ blockedReady: currentUser.blockedReady,
61826
62321
  online: state.online,
61827
62322
  chatPK: session?.chatPK || currentUser.chatPK || "",
61828
62323
  chatSigningPK: session?.chatSigningPK || currentUser.chatSigningPK || "",
@@ -61855,13 +62350,12 @@ function openAccount(options = {}) {
61855
62350
  }
61856
62351
  function syncPeers() {
61857
62352
  const currentUser = state.user || {};
61858
- const cachedBlacklist = state.session?.localCache?.read?.()?.blacklist;
61859
62353
  const chatSnapshot = chat.getSnapshot();
61860
62354
  peers.setSources({
61861
62355
  chatPeers: chatSnapshot.peers,
61862
62356
  chats: chatSnapshot.chats,
61863
- blocked: currentUser.blockedReady ? currentUser.blocked : cachedBlacklist || [],
61864
- blockedReady: currentUser.blockedReady || Array.isArray(cachedBlacklist),
62357
+ blocked: currentUser.blocked,
62358
+ blockedReady: currentUser.blockedReady,
61865
62359
  chatPK: state.session?.chatPK || currentUser.chatPK || "",
61866
62360
  chatSigningPK: state.session?.chatSigningPK || currentUser.chatSigningPK || "",
61867
62361
  walletPeers: wallet.getPeers(),
@@ -62146,8 +62640,9 @@ function openAccount(options = {}) {
62146
62640
  hasVault: !!vault,
62147
62641
  fromCache: !!info.fromCache
62148
62642
  });
62643
+ const currentVault = state.vault && vault && equalBytes(state.vault, vault) ? state.vault : vault || null;
62149
62644
  emit2({
62150
- vault: vault || null,
62645
+ vault: currentVault,
62151
62646
  vaultFromCache: false,
62152
62647
  vaultReady: state.vaultReady || !info.fromCache,
62153
62648
  vaultError: info.fromCache ? state.vaultError : null
@@ -62257,7 +62752,7 @@ function openAccount(options = {}) {
62257
62752
  function syncBootstrap() {
62258
62753
  if (closed || !state.session || state.session.closed || !serverReady())
62259
62754
  return;
62260
- if (state.user.blockedReady) {
62755
+ if (state.user.blockedReady && !state.user.blockedPending?.length) {
62261
62756
  const cache = state.session.localCache;
62262
62757
  const blocked = state.user.blocked;
62263
62758
  if (JSON.stringify(cache?.read?.()?.blacklist) !== JSON.stringify(blocked)) {
@@ -62397,6 +62892,7 @@ function openAccount(options = {}) {
62397
62892
  });
62398
62893
  if (!isCurrent())
62399
62894
  throw new Error("account changed during unlock");
62895
+ user.restoreBlocked({ uid, blocked: session.localCache?.read?.()?.blacklist });
62400
62896
  chatSession = session;
62401
62897
  chat.setSources(chatSources());
62402
62898
  unlockStage = "launching";
@@ -62591,15 +63087,21 @@ function openAccount(options = {}) {
62591
63087
  if (!observedVault)
62592
63088
  throw new Error("vault not ready");
62593
63089
  const isCurrent = () => !closed && !operationBarrier.blocksOperations && state.uid === uid && state.session === session && state.vault === observedVault && !session.closed && cloud.auth.user?.uid === uid;
63090
+ markDiag(diag, "account.delete.phase", { phase: "verify-vault" });
62594
63091
  const verified = await vaultCrypto.verifyVaultPassword(observedVault, password);
62595
63092
  if (!verified)
62596
63093
  throw new Error("incorrect password");
62597
63094
  if (!isCurrent())
62598
63095
  throw new Error("account changed during deletion");
63096
+ await waitForOnlineDeletion(isCurrent);
63097
+ markDiag(diag, "account.delete.phase", { phase: "retire-chats" });
62599
63098
  const chats = await chat.getSnapshot().deleteAllChats();
62600
63099
  if (!isCurrent())
62601
63100
  throw new Error("account changed during deletion");
63101
+ await waitForOnlineDeletion(isCurrent);
63102
+ markDiag(diag, "account.delete.phase", { phase: "delete-account" });
62602
63103
  await cloud.user.delete();
63104
+ markDiag(diag, "account.delete.phase", { phase: "clear-local-state" });
62603
63105
  try {
62604
63106
  await localCache?.clear?.();
62605
63107
  } catch (error) {
@@ -62623,6 +63125,31 @@ function openAccount(options = {}) {
62623
63125
  finishOperation();
62624
63126
  }
62625
63127
  }
63128
+ function waitForOnlineDeletion(isCurrent) {
63129
+ if (!isCurrent())
63130
+ return Promise.reject(new Error("account changed during deletion"));
63131
+ if (state.online)
63132
+ return Promise.resolve();
63133
+ return new Promise((resolve, reject) => {
63134
+ const finish = (error) => {
63135
+ clearTimeout(timer);
63136
+ listeners.delete(check);
63137
+ if (error)
63138
+ reject(error);
63139
+ else
63140
+ resolve();
63141
+ };
63142
+ const check = () => {
63143
+ if (!isCurrent())
63144
+ finish(new Error("account changed during deletion"));
63145
+ else if (state.online)
63146
+ finish();
63147
+ };
63148
+ const timer = setTimeout(() => finish(new Error("couldnt connect to server")), ACCOUNT_CONNECTION_UNAVAILABLE_MS);
63149
+ listeners.add(check);
63150
+ check();
63151
+ });
63152
+ }
62626
63153
  async function switchNetwork(network) {
62627
63154
  if (state.lockState !== "unlocked") {
62628
63155
  throw new Error("unlock the vault before switching networks");