@unicitylabs/sphere-sdk 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -30,6 +30,8 @@ interface GroupData {
30
30
  unreadCount?: number;
31
31
  lastMessageTime?: number;
32
32
  lastMessageText?: string;
33
+ /** Only admins and moderators can post; other members are read-only (NIP-29 "write-restricted" tag) */
34
+ writeRestricted?: boolean;
33
35
  /** When the current user joined this group locally (used to filter old events) */
34
36
  localJoinedAt?: number;
35
37
  }
@@ -62,11 +64,24 @@ interface GroupChatModuleConfig {
62
64
  /** Max reconnect attempts (default: 5) */
63
65
  maxReconnectAttempts?: number;
64
66
  }
67
+ interface GroupMessagesPage {
68
+ messages: GroupMessageData[];
69
+ hasMore: boolean;
70
+ oldestTimestamp: number | null;
71
+ }
72
+ interface GetGroupMessagesPageOptions {
73
+ /** Max messages to return (default: 20) */
74
+ limit?: number;
75
+ /** Return messages older than this timestamp */
76
+ before?: number;
77
+ }
65
78
  interface CreateGroupOptions {
66
79
  name: string;
67
80
  description?: string;
68
81
  picture?: string;
69
82
  visibility?: GroupVisibility;
83
+ /** Only admins and moderators can post; other members are read-only */
84
+ writeRestricted?: boolean;
70
85
  }
71
86
 
72
87
  /**
@@ -1299,7 +1314,7 @@ interface TrackedAddress extends TrackedAddressEntry {
1299
1314
  /** Primary nametag (from nametag cache, without @ prefix) */
1300
1315
  readonly nametag?: string;
1301
1316
  }
1302
- type SphereEventType = 'transfer:incoming' | 'transfer:confirmed' | 'transfer:failed' | 'payment_request:incoming' | 'payment_request:accepted' | 'payment_request:rejected' | 'payment_request:paid' | 'payment_request:response' | 'message:dm' | 'message:read' | 'message:typing' | 'composing:started' | 'message:broadcast' | 'sync:started' | 'sync:completed' | 'sync:provider' | 'sync:error' | 'connection:changed' | 'nametag:registered' | 'nametag:recovered' | 'identity:changed' | 'address:activated' | 'address:hidden' | 'address:unhidden' | 'sync:remote-update' | 'groupchat:message' | 'groupchat:joined' | 'groupchat:left' | 'groupchat:kicked' | 'groupchat:group_deleted' | 'groupchat:updated' | 'groupchat:connection' | 'history:updated';
1317
+ type SphereEventType = 'transfer:incoming' | 'transfer:confirmed' | 'transfer:failed' | 'payment_request:incoming' | 'payment_request:accepted' | 'payment_request:rejected' | 'payment_request:paid' | 'payment_request:response' | 'message:dm' | 'message:read' | 'message:typing' | 'composing:started' | 'message:broadcast' | 'sync:started' | 'sync:completed' | 'sync:provider' | 'sync:error' | 'connection:changed' | 'nametag:registered' | 'nametag:recovered' | 'identity:changed' | 'address:activated' | 'address:hidden' | 'address:unhidden' | 'sync:remote-update' | 'groupchat:message' | 'groupchat:joined' | 'groupchat:left' | 'groupchat:kicked' | 'groupchat:group_deleted' | 'groupchat:updated' | 'groupchat:connection' | 'groupchat:ready' | 'communications:ready' | 'history:updated';
1303
1318
  interface SphereEventMap {
1304
1319
  'transfer:incoming': IncomingTransfer;
1305
1320
  'transfer:confirmed': TransferResult;
@@ -1399,6 +1414,12 @@ interface SphereEventMap {
1399
1414
  'groupchat:connection': {
1400
1415
  connected: boolean;
1401
1416
  };
1417
+ 'groupchat:ready': {
1418
+ groupCount: number;
1419
+ };
1420
+ 'communications:ready': {
1421
+ conversationCount: number;
1422
+ };
1402
1423
  'history:updated': TransactionHistoryEntry;
1403
1424
  }
1404
1425
  type SphereEventHandler<T extends SphereEventType> = (data: SphereEventMap[T]) => void;
@@ -1963,6 +1984,14 @@ interface TransportProvider extends BaseProvider {
1963
1984
  * and resolves after EOSE (End Of Stored Events).
1964
1985
  */
1965
1986
  fetchPendingEvents?(): Promise<void>;
1987
+ /**
1988
+ * Register a handler to be called when the chat subscription receives EOSE
1989
+ * (End Of Stored Events), indicating that historical DMs have been delivered.
1990
+ * The handler fires at most once per subscription lifecycle.
1991
+ *
1992
+ * @returns Unsubscribe function
1993
+ */
1994
+ onChatReady?(handler: () => void): () => void;
1966
1995
  }
1967
1996
  /**
1968
1997
  * Payload for sending instant split bundles
@@ -3652,6 +3681,7 @@ declare class GroupChatModule {
3652
3681
  getGroups(): GroupData[];
3653
3682
  getGroup(groupId: string): GroupData | null;
3654
3683
  getMessages(groupId: string): GroupMessageData[];
3684
+ getMessagesPage(groupId: string, options?: GetGroupMessagesPageOptions): GroupMessagesPage;
3655
3685
  getMembers(groupId: string): GroupMemberData[];
3656
3686
  getMember(groupId: string, pubkey: string): GroupMemberData | null;
3657
3687
  getTotalUnreadCount(): number;
@@ -3667,6 +3697,12 @@ declare class GroupChatModule {
3667
3697
  */
3668
3698
  canModerateGroup(groupId: string): Promise<boolean>;
3669
3699
  isCurrentUserRelayAdmin(): Promise<boolean>;
3700
+ /**
3701
+ * Check if current user can write messages to a group.
3702
+ * For write-restricted groups, only admins/moderators can post.
3703
+ * For normal groups, any member can post.
3704
+ */
3705
+ canWriteToGroup(groupId: string): boolean;
3670
3706
  getCurrentUserRole(groupId: string): GroupRole | null;
3671
3707
  onMessage(handler: (message: GroupMessageData) => void): () => void;
3672
3708
  getRelayUrls(): string[];
package/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ interface GroupData {
30
30
  unreadCount?: number;
31
31
  lastMessageTime?: number;
32
32
  lastMessageText?: string;
33
+ /** Only admins and moderators can post; other members are read-only (NIP-29 "write-restricted" tag) */
34
+ writeRestricted?: boolean;
33
35
  /** When the current user joined this group locally (used to filter old events) */
34
36
  localJoinedAt?: number;
35
37
  }
@@ -62,11 +64,24 @@ interface GroupChatModuleConfig {
62
64
  /** Max reconnect attempts (default: 5) */
63
65
  maxReconnectAttempts?: number;
64
66
  }
67
+ interface GroupMessagesPage {
68
+ messages: GroupMessageData[];
69
+ hasMore: boolean;
70
+ oldestTimestamp: number | null;
71
+ }
72
+ interface GetGroupMessagesPageOptions {
73
+ /** Max messages to return (default: 20) */
74
+ limit?: number;
75
+ /** Return messages older than this timestamp */
76
+ before?: number;
77
+ }
65
78
  interface CreateGroupOptions {
66
79
  name: string;
67
80
  description?: string;
68
81
  picture?: string;
69
82
  visibility?: GroupVisibility;
83
+ /** Only admins and moderators can post; other members are read-only */
84
+ writeRestricted?: boolean;
70
85
  }
71
86
 
72
87
  /**
@@ -1299,7 +1314,7 @@ interface TrackedAddress extends TrackedAddressEntry {
1299
1314
  /** Primary nametag (from nametag cache, without @ prefix) */
1300
1315
  readonly nametag?: string;
1301
1316
  }
1302
- type SphereEventType = 'transfer:incoming' | 'transfer:confirmed' | 'transfer:failed' | 'payment_request:incoming' | 'payment_request:accepted' | 'payment_request:rejected' | 'payment_request:paid' | 'payment_request:response' | 'message:dm' | 'message:read' | 'message:typing' | 'composing:started' | 'message:broadcast' | 'sync:started' | 'sync:completed' | 'sync:provider' | 'sync:error' | 'connection:changed' | 'nametag:registered' | 'nametag:recovered' | 'identity:changed' | 'address:activated' | 'address:hidden' | 'address:unhidden' | 'sync:remote-update' | 'groupchat:message' | 'groupchat:joined' | 'groupchat:left' | 'groupchat:kicked' | 'groupchat:group_deleted' | 'groupchat:updated' | 'groupchat:connection' | 'history:updated';
1317
+ type SphereEventType = 'transfer:incoming' | 'transfer:confirmed' | 'transfer:failed' | 'payment_request:incoming' | 'payment_request:accepted' | 'payment_request:rejected' | 'payment_request:paid' | 'payment_request:response' | 'message:dm' | 'message:read' | 'message:typing' | 'composing:started' | 'message:broadcast' | 'sync:started' | 'sync:completed' | 'sync:provider' | 'sync:error' | 'connection:changed' | 'nametag:registered' | 'nametag:recovered' | 'identity:changed' | 'address:activated' | 'address:hidden' | 'address:unhidden' | 'sync:remote-update' | 'groupchat:message' | 'groupchat:joined' | 'groupchat:left' | 'groupchat:kicked' | 'groupchat:group_deleted' | 'groupchat:updated' | 'groupchat:connection' | 'groupchat:ready' | 'communications:ready' | 'history:updated';
1303
1318
  interface SphereEventMap {
1304
1319
  'transfer:incoming': IncomingTransfer;
1305
1320
  'transfer:confirmed': TransferResult;
@@ -1399,6 +1414,12 @@ interface SphereEventMap {
1399
1414
  'groupchat:connection': {
1400
1415
  connected: boolean;
1401
1416
  };
1417
+ 'groupchat:ready': {
1418
+ groupCount: number;
1419
+ };
1420
+ 'communications:ready': {
1421
+ conversationCount: number;
1422
+ };
1402
1423
  'history:updated': TransactionHistoryEntry;
1403
1424
  }
1404
1425
  type SphereEventHandler<T extends SphereEventType> = (data: SphereEventMap[T]) => void;
@@ -1963,6 +1984,14 @@ interface TransportProvider extends BaseProvider {
1963
1984
  * and resolves after EOSE (End Of Stored Events).
1964
1985
  */
1965
1986
  fetchPendingEvents?(): Promise<void>;
1987
+ /**
1988
+ * Register a handler to be called when the chat subscription receives EOSE
1989
+ * (End Of Stored Events), indicating that historical DMs have been delivered.
1990
+ * The handler fires at most once per subscription lifecycle.
1991
+ *
1992
+ * @returns Unsubscribe function
1993
+ */
1994
+ onChatReady?(handler: () => void): () => void;
1966
1995
  }
1967
1996
  /**
1968
1997
  * Payload for sending instant split bundles
@@ -3652,6 +3681,7 @@ declare class GroupChatModule {
3652
3681
  getGroups(): GroupData[];
3653
3682
  getGroup(groupId: string): GroupData | null;
3654
3683
  getMessages(groupId: string): GroupMessageData[];
3684
+ getMessagesPage(groupId: string, options?: GetGroupMessagesPageOptions): GroupMessagesPage;
3655
3685
  getMembers(groupId: string): GroupMemberData[];
3656
3686
  getMember(groupId: string, pubkey: string): GroupMemberData | null;
3657
3687
  getTotalUnreadCount(): number;
@@ -3667,6 +3697,12 @@ declare class GroupChatModule {
3667
3697
  */
3668
3698
  canModerateGroup(groupId: string): Promise<boolean>;
3669
3699
  isCurrentUserRelayAdmin(): Promise<boolean>;
3700
+ /**
3701
+ * Check if current user can write messages to a group.
3702
+ * For write-restricted groups, only admins/moderators can post.
3703
+ * For normal groups, any member can post.
3704
+ */
3705
+ canWriteToGroup(groupId: string): boolean;
3670
3706
  getCurrentUserRole(groupId: string): GroupRole | null;
3671
3707
  onMessage(handler: (message: GroupMessageData) => void): () => void;
3672
3708
  getRelayUrls(): string[];
package/dist/index.js CHANGED
@@ -8594,6 +8594,12 @@ var CommunicationsModule = class {
8594
8594
  this.unsubscribeComposing = deps.transport.onComposing?.((indicator) => {
8595
8595
  this.handleComposingIndicator(indicator);
8596
8596
  }) ?? null;
8597
+ if (deps.transport.onChatReady) {
8598
+ deps.transport.onChatReady(() => {
8599
+ const conversations = this.getConversations();
8600
+ deps.emitEvent("communications:ready", { conversationCount: conversations.size });
8601
+ });
8602
+ }
8597
8603
  }
8598
8604
  /**
8599
8605
  * Load messages from storage.
@@ -9240,6 +9246,7 @@ var GroupChatModule = class {
9240
9246
  await this.subscribeToJoinedGroups();
9241
9247
  }
9242
9248
  this.deps.emitEvent("groupchat:connection", { connected: true });
9249
+ this.deps.emitEvent("groupchat:ready", { groupCount: this.groups.size });
9243
9250
  } catch (error) {
9244
9251
  logger.error("GroupChat", "Failed to connect to relays", error);
9245
9252
  this.deps.emitEvent("groupchat:connection", { connected: false });
@@ -9358,17 +9365,23 @@ var GroupChatModule = class {
9358
9365
  const group = this.groups.get(groupId);
9359
9366
  if (!group) return;
9360
9367
  if (event.kind === NIP29_KINDS.GROUP_METADATA) {
9361
- if (!event.content || event.content.trim() === "") return;
9362
- try {
9363
- const metadata = JSON.parse(event.content);
9364
- group.name = metadata.name || group.name;
9365
- group.description = metadata.about || group.description;
9366
- group.picture = metadata.picture || group.picture;
9367
- group.updatedAt = event.created_at * 1e3;
9368
- this.groups.set(groupId, group);
9369
- this.persistGroups();
9370
- } catch {
9368
+ if (event.content && event.content.trim()) {
9369
+ try {
9370
+ const metadata = JSON.parse(event.content);
9371
+ group.name = metadata.name || group.name;
9372
+ group.description = metadata.about || group.description;
9373
+ group.picture = metadata.picture || group.picture;
9374
+ if (metadata["write-restricted"] === true) group.writeRestricted = true;
9375
+ else group.writeRestricted = void 0;
9376
+ } catch {
9377
+ }
9371
9378
  }
9379
+ for (const tag of event.tags) {
9380
+ if (tag[0] === "write-restricted") group.writeRestricted = true;
9381
+ }
9382
+ group.updatedAt = event.created_at * 1e3;
9383
+ this.groups.set(groupId, group);
9384
+ this.persistGroups();
9372
9385
  } else if (event.kind === NIP29_KINDS.GROUP_MEMBERS) {
9373
9386
  this.updateMembersFromEvent(groupId, event);
9374
9387
  } else if (event.kind === NIP29_KINDS.GROUP_ADMINS) {
@@ -9672,7 +9685,8 @@ var GroupChatModule = class {
9672
9685
  picture: options.picture,
9673
9686
  closed: true,
9674
9687
  private: isPrivate,
9675
- hidden: isPrivate
9688
+ hidden: isPrivate,
9689
+ ...options.writeRestricted ? { "write-restricted": true } : {}
9676
9690
  })
9677
9691
  });
9678
9692
  if (!eventId) return null;
@@ -9867,6 +9881,19 @@ var GroupChatModule = class {
9867
9881
  getMessages(groupId) {
9868
9882
  return (this.messages.get(groupId) || []).sort((a, b) => a.timestamp - b.timestamp);
9869
9883
  }
9884
+ getMessagesPage(groupId, options) {
9885
+ const limit = options?.limit ?? 20;
9886
+ const before = options?.before ?? Infinity;
9887
+ const groupMessages = this.messages.get(groupId) ?? [];
9888
+ const filtered = groupMessages.filter((m) => m.timestamp < before).sort((a, b) => b.timestamp - a.timestamp);
9889
+ const page = filtered.slice(0, limit);
9890
+ return {
9891
+ messages: page.reverse(),
9892
+ // chronological order
9893
+ hasMore: filtered.length > limit,
9894
+ oldestTimestamp: page.length > 0 ? page[0].timestamp : null
9895
+ };
9896
+ }
9870
9897
  getMembers(groupId) {
9871
9898
  return (this.members.get(groupId) || []).sort((a, b) => a.joinedAt - b.joinedAt);
9872
9899
  }
@@ -9973,6 +10000,17 @@ var GroupChatModule = class {
9973
10000
  const admins = await this.fetchRelayAdmins();
9974
10001
  return admins.has(myPubkey);
9975
10002
  }
10003
+ /**
10004
+ * Check if current user can write messages to a group.
10005
+ * For write-restricted groups, only admins/moderators can post.
10006
+ * For normal groups, any member can post.
10007
+ */
10008
+ canWriteToGroup(groupId) {
10009
+ const group = this.groups.get(groupId);
10010
+ if (!group) return false;
10011
+ if (!group.writeRestricted) return true;
10012
+ return this.isCurrentUserModerator(groupId);
10013
+ }
9976
10014
  getCurrentUserRole(groupId) {
9977
10015
  const myPubkey = this.getMyPublicKey();
9978
10016
  if (!myPubkey) return null;
@@ -10347,6 +10385,7 @@ var GroupChatModule = class {
10347
10385
  let description;
10348
10386
  let picture;
10349
10387
  let isPrivate = false;
10388
+ let writeRestricted = false;
10350
10389
  if (event.content && event.content.trim()) {
10351
10390
  try {
10352
10391
  const metadata = JSON.parse(event.content);
@@ -10354,6 +10393,7 @@ var GroupChatModule = class {
10354
10393
  description = metadata.about || metadata.description;
10355
10394
  picture = metadata.picture;
10356
10395
  isPrivate = metadata.private === true;
10396
+ if (metadata["write-restricted"] === true) writeRestricted = true;
10357
10397
  } catch {
10358
10398
  }
10359
10399
  }
@@ -10363,6 +10403,7 @@ var GroupChatModule = class {
10363
10403
  if (tag[0] === "picture" && tag[1]) picture = tag[1];
10364
10404
  if (tag[0] === "private") isPrivate = true;
10365
10405
  if (tag[0] === "public" && tag[1] === "false") isPrivate = true;
10406
+ if (tag[0] === "write-restricted") writeRestricted = true;
10366
10407
  }
10367
10408
  return {
10368
10409
  id: groupId,
@@ -10371,6 +10412,7 @@ var GroupChatModule = class {
10371
10412
  description,
10372
10413
  picture,
10373
10414
  visibility: isPrivate ? GroupVisibility.PRIVATE : GroupVisibility.PUBLIC,
10415
+ writeRestricted: writeRestricted || void 0,
10374
10416
  createdAt: event.created_at * 1e3
10375
10417
  };
10376
10418
  } catch {