@openmarket/rooms-client 0.6.0 → 0.7.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.
@@ -26,6 +26,20 @@ import type {
26
26
  /** The reserved bucket key for untopicked (linear) messages. */
27
27
  export const UNTOPICKED_BUCKET = "";
28
28
 
29
+ /** Effective attention for a bucket: an elapsed timed mute reads as the
30
+ * base attention ('default'). The store expires timed mutes lazily (no
31
+ * deadline-triggered frame), so a hydrated inbox would otherwise stay
32
+ * muted past the deadline until the next hydration. A null or absent
33
+ * deadline keeps the stored attention (permanent mute). */
34
+ export function resolveTopicAttention(
35
+ state: Pick<RoomTopicBucketState, "attention" | "muteUntil">,
36
+ nowMs: number = Date.now(),
37
+ ): RoomTopicAttention {
38
+ if (state.attention !== "mute" || state.muteUntil == null) return state.attention;
39
+ const deadline = Date.parse(state.muteUntil);
40
+ return Number.isFinite(deadline) && deadline <= nowMs ? "default" : "mute";
41
+ }
42
+
29
43
  export interface TopicInboxItem {
30
44
  /** Bucket key: a topicId, or UNTOPICKED_BUCKET for the linear lane. */
31
45
  key: string;
@@ -141,7 +155,7 @@ export class RoomTopicInbox {
141
155
  }
142
156
 
143
157
  attentionFor(topicId: string): RoomTopicAttention {
144
- return this.bucketFor(topicId).attention;
158
+ return resolveTopicAttention(this.bucketFor(topicId));
145
159
  }
146
160
 
147
161
  /** A registry mutation arrived (TOPIC_UPDATED, or a verb result echo). */
@@ -196,6 +210,10 @@ export class RoomTopicInbox {
196
210
  this.buckets.set(topicId, bucket);
197
211
  }
198
212
  bucket.attention = attention;
213
+ // The mirror knows only the new attention: a stale deadline from a
214
+ // previous timed mute must not expire it. The next hydration restores
215
+ // any server-side deadline.
216
+ delete bucket.muteUntil;
199
217
  }
200
218
 
201
219
  /**
@@ -224,7 +242,7 @@ export class RoomTopicInbox {
224
242
  channelUnread(): number {
225
243
  let total = 0;
226
244
  for (const bucket of this.buckets.values()) {
227
- if (bucket.attention === "mute") continue;
245
+ if (resolveTopicAttention(bucket) === "mute") continue;
228
246
  total += bucket.unreadCount;
229
247
  }
230
248
  return total;
@@ -241,7 +259,7 @@ export class RoomTopicInbox {
241
259
  inboxItems(): TopicInboxItem[] {
242
260
  const items: TopicInboxItem[] = [];
243
261
  const untopicked = this.bucketFor(UNTOPICKED_BUCKET);
244
- if (untopicked.unreadCount > 0 && untopicked.attention !== "mute") {
262
+ if (untopicked.unreadCount > 0 && resolveTopicAttention(untopicked) !== "mute") {
245
263
  items.push({
246
264
  key: UNTOPICKED_BUCKET,
247
265
  topic: undefined,
@@ -254,11 +272,11 @@ export class RoomTopicInbox {
254
272
  for (const topic of this.topics.values()) {
255
273
  if (topic.mergedInto) continue;
256
274
  const state = this.bucketFor(topic.topicId);
257
- if (state.attention === "mute") continue;
275
+ if (resolveTopicAttention(state) === "mute") continue;
258
276
  let aliasUnread = 0;
259
277
  for (const alias of this.aliasesOf(topic.topicId)) {
260
278
  const aliasState = this.bucketFor(alias);
261
- if (aliasState.attention === "mute") continue;
279
+ if (resolveTopicAttention(aliasState) === "mute") continue;
262
280
  aliasUnread += aliasState.unreadCount;
263
281
  }
264
282
  if (state.unreadCount + aliasUnread === 0) continue;
@@ -250,6 +250,19 @@ export interface RoomLedgerEntry {
250
250
  since?: string | number | null | undefined;
251
251
  reactions?: Array<{ emoji: string; count: number }> | undefined;
252
252
  editedAt?: string | number | Date | undefined;
253
+ /** Database-serialized edit counter: positive integer, strictly
254
+ * increasing per event row, incremented on every successful text or
255
+ * caption edit; absent means never edited. Orders caption edits where
256
+ * wall-clock editedAt stamps can tie or skew across store replicas.
257
+ * Missing on stores older than edit revisions. */
258
+ editRevision?: number | undefined;
259
+ /** Database-serialized card-mutation counter: positive integer,
260
+ * strictly increasing per event row, bumped on every unfurl card
261
+ * mutation (attach, edit-drop, purge); absent means cards were never
262
+ * mutated. Consumers apply card patches only at or above their known
263
+ * revision, making stale post-removal unfurl frames inert. Missing on
264
+ * stores older than unfurl revisions. */
265
+ unfurlsRevision?: number | undefined;
253
266
  deletedAt?: string | number | Date | undefined;
254
267
  deletedBy?: string | undefined;
255
268
  replyCount?: number | undefined;
@@ -258,6 +271,12 @@ export interface RoomLedgerEntry {
258
271
  pinnedBy?: string | null | undefined;
259
272
  pinnedAt?: string | number | Date | null | undefined;
260
273
  attachments?: RoomAttachment[] | undefined;
274
+ /** Counts-only poll summary when this message carries a poll. Additive:
275
+ * non-poll entries omit it, and live updates ride ENTRY_UPDATED. */
276
+ poll?: RoomPollSummary | null | undefined;
277
+ /** Server-rendered link unfurl cards. Additive: entries without links
278
+ * (and servers older than the unfurl worker) omit it. */
279
+ unfurls?: RoomUnfurl[] | undefined;
261
280
  roomRole?: RoomRole | null | undefined;
262
281
  metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
263
282
  }
@@ -313,6 +332,49 @@ export interface RoomAttachment {
313
332
  */
314
333
  export type RoomAttachmentInput = RoomAttachment & { key: string };
315
334
 
335
+ export interface RoomPollOption {
336
+ id: string;
337
+ text: string;
338
+ /** Vote count for this option. Counts only, never voter identities
339
+ * (voters are fetched on demand, exactly like reactors). */
340
+ count: number;
341
+ }
342
+
343
+ /**
344
+ * Counts-only poll summary denormalized onto the carrying message (a poll
345
+ * IS a message). Vote/close/edit updates ride the existing ENTRY_UPDATED
346
+ * frame with the refreshed summary; the server's side collections stay the
347
+ * authoritative copy.
348
+ */
349
+ export interface RoomPollSummary {
350
+ question: string;
351
+ options: RoomPollOption[];
352
+ multi: boolean;
353
+ /** ISO date when the poll auto-closes; null or absent means open-ended. */
354
+ closesAt?: string | null | undefined;
355
+ closed: boolean;
356
+ /** Store-serialized poll ordering revision, strictly increasing per
357
+ * poll: consumers reject frames below their known revision so
358
+ * bus-reordered vote/close updates cannot regress tallies. Present
359
+ * where the relay forwards it; missing on older relays. */
360
+ revision?: number | undefined;
361
+ }
362
+
363
+ /**
364
+ * Server-rendered link unfurl card on an entry. `url` is the canonical
365
+ * URL; `thumbnailKey` is the re-hosted copy in the attachments bucket,
366
+ * signed through the member-scoped attachment path. The origin's image URL
367
+ * never crosses the wire. Cards appear via ENTRY_UPDATED when the unfurl
368
+ * worker lands.
369
+ */
370
+ export interface RoomUnfurl {
371
+ url: string;
372
+ title?: string | undefined;
373
+ description?: string | undefined;
374
+ siteName?: string | undefined;
375
+ thumbnailKey?: string | undefined;
376
+ }
377
+
316
378
  /**
317
379
  * A registry-backed topic inside a channel. The id is permanent: rename
318
380
  * changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
@@ -383,6 +445,21 @@ export interface RoomReadStatePayload {
383
445
  * Present when the store surfaces it and on ROOM_SET_ATTENTION acks
384
446
  * (which reply READ_STATE scoped to the one room, readState empty). */
385
447
  attention?: Record<string, RoomTopicAttention> | undefined;
448
+ /** Per-room timed-mute expiry (ISO date), keyed like attention. Rides
449
+ * beside a 'mute' attention entry when the mute carries an expiry; null
450
+ * marks an explicitly cleared or permanent mute. A lapsed expiry never
451
+ * appears: the server resolves it at read time and the attention value
452
+ * reads as its base ('default'). Missing on servers older than
453
+ * mute-with-duration. */
454
+ muteUntil?: Record<string, string | null> | undefined;
455
+ /** Per-room attention commit revision, keyed like attention: a
456
+ * database-serialized, strictly increasing counter per (user, room),
457
+ * present only for rooms whose attention has ever been committed.
458
+ * ORDERING CONTRACT: a consumer that has synced revision R for a room
459
+ * must ignore snapshot attention entries whose revision is <= R (a
460
+ * snapshot read before a newer commit can arrive after that commit's
461
+ * sync frame). Missing on stores older than attention revisions. */
462
+ attentionRevision?: Record<string, number> | undefined;
386
463
  /** Unread mention counts by room, server-computed from the notify
387
464
  * worker's ROOM_MENTION rows so badges survive reload and clear through
388
465
  * the ack path. Unlike readState/attention the map is SPARSE AND
@@ -995,6 +1072,14 @@ export interface RoomTopicBucketState {
995
1072
  unreadCount: number;
996
1073
  cursor: number;
997
1074
  attention: RoomTopicAttention;
1075
+ /** Standing timed-mute deadline (ISO date; null = permanent mute).
1076
+ * Present only on the room-wide '' bucket and only while its attention
1077
+ * is an active 'mute'; never on topic buckets. Same semantics as the
1078
+ * read-state muteUntil: the store expires timed mutes lazily, so
1079
+ * consumers resolve an elapsed deadline back to the base attention
1080
+ * ('default') at read time (see resolveTopicAttention). Missing on
1081
+ * servers older than mute-with-duration. */
1082
+ muteUntil?: string | null | undefined;
998
1083
  }
999
1084
 
1000
1085
  export interface RoomTopicResultPayload {