@tagea/capacitor-matrix 1.0.1 → 1.1.1

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/README.md CHANGED
@@ -1280,6 +1280,7 @@ removeAllListeners() => Promise<void>
1280
1280
  | **`roomId`** | <code>string</code> | |
1281
1281
  | **`senderId`** | <code>string</code> | |
1282
1282
  | **`type`** | <code>string</code> | |
1283
+ | **`stateKey`** | <code>string</code> | State key for state events (e.g. target user ID for m.room.member) |
1283
1284
  | **`content`** | <code><a href="#record">Record</a>&lt;string, unknown&gt;</code> | |
1284
1285
  | **`originServerTs`** | <code>number</code> | |
1285
1286
  | **`status`** | <code>'sending' \| 'sent' \| 'delivered' \| 'read'</code> | Delivery/read status for own messages: 'sending' \| 'sent' \| 'delivered' \| 'read' |
@@ -65,6 +65,8 @@ class MatrixSDKBridge(private val context: Context) {
65
65
  private val timelineListenerHandles = mutableListOf<Any>()
66
66
  // Rooms currently being paginated by getRoomMessages — live listener suppresses events for these
67
67
  private val paginatingRooms = Collections.synchronizedSet(mutableSetOf<String>())
68
+ // Per-room tracking of the oldest event ID returned to JS, used for pagination cursor
69
+ private val oldestReturnedEventId = mutableMapOf<String, String>()
68
70
  private var receiptSyncJob: Job? = null
69
71
  // Receipt cache: roomId → (eventId → set of userIds who sent a read receipt)
70
72
  // Populated by the parallel v2 receipt sync since sliding sync doesn't deliver
@@ -108,6 +110,15 @@ class MatrixSDKBridge(private val context: Context) {
108
110
  userId: String,
109
111
  deviceId: String,
110
112
  ): SessionInfo {
113
+ // Stop existing sync and clean up stale references before replacing the client
114
+ syncService?.stop()
115
+ syncService = null
116
+ receiptSyncJob?.cancel()
117
+ receiptSyncJob = null
118
+ timelineListenerHandles.clear()
119
+ roomTimelines.clear()
120
+ subscribedRoomIds.clear()
121
+
111
122
  val safeUserId = userId.replace(Regex("[^a-zA-Z0-9_.-]"), "_")
112
123
  val dataDir = context.filesDir.resolve("matrix_sdk/$safeUserId")
113
124
  dataDir.mkdirs()
@@ -223,6 +234,15 @@ class MatrixSDKBridge(private val context: Context) {
223
234
  try {
224
235
  val timeline = getOrCreateTimeline(room)
225
236
  val handle = timeline.addListener(object : TimelineListener {
237
+ private fun emitRoomUpdate() {
238
+ scope.launch {
239
+ val unreadCount = try {
240
+ room.roomInfo().numUnreadMessages?.toInt() ?: 0
241
+ } catch (_: Exception) { 0 }
242
+ onRoomUpdate(roomId, mapOf("roomId" to roomId, "unreadCount" to unreadCount))
243
+ }
244
+ }
245
+
226
246
  override fun onUpdate(diff: List<TimelineDiff>) {
227
247
  // Suppress live events while getRoomMessages is paginating this room
228
248
  if (paginatingRooms.contains(roomId)) return
@@ -235,13 +255,13 @@ class MatrixSDKBridge(private val context: Context) {
235
255
  if (!isLocalEcho) {
236
256
  serializeTimelineItem(d.value, roomId)?.let { onMessage(it) }
237
257
  }
238
- onRoomUpdate(roomId, mapOf("roomId" to roomId))
258
+ emitRoomUpdate()
239
259
  }
240
260
  is TimelineDiff.Append -> {
241
261
  d.values.forEach { item ->
242
262
  serializeTimelineItem(item, roomId)?.let { onMessage(it) }
243
263
  }
244
- onRoomUpdate(roomId, mapOf("roomId" to roomId))
264
+ emitRoomUpdate()
245
265
  }
246
266
  is TimelineDiff.Set -> {
247
267
  serializeTimelineItem(d.value, roomId)?.let { onMessage(it) }
@@ -250,15 +270,15 @@ class MatrixSDKBridge(private val context: Context) {
250
270
  d.values.forEach { item ->
251
271
  serializeTimelineItem(item, roomId)?.let { onMessage(it) }
252
272
  }
253
- onRoomUpdate(roomId, mapOf("roomId" to roomId))
273
+ emitRoomUpdate()
254
274
  }
255
275
  is TimelineDiff.Insert -> {
256
276
  serializeTimelineItem(d.value, roomId)?.let { onMessage(it) }
257
- onRoomUpdate(roomId, mapOf("roomId" to roomId))
277
+ emitRoomUpdate()
258
278
  }
259
279
  is TimelineDiff.PushFront -> {
260
280
  serializeTimelineItem(d.value, roomId)?.let { onMessage(it) }
261
- onRoomUpdate(roomId, mapOf("roomId" to roomId))
281
+ emitRoomUpdate()
262
282
  }
263
283
  else -> { /* Remove, Clear, Truncate, PopBack, PopFront — no JS event needed */ }
264
284
  }
@@ -416,13 +436,21 @@ class MatrixSDKBridge(private val context: Context) {
416
436
  val collector = TimelineItemCollector(roomId)
417
437
  val handle = timeline.addListener(collector)
418
438
 
439
+ val isPagination = from != null
440
+ var hitStart = false
419
441
  try {
420
442
  // Wait for the initial Reset snapshot before paginating
421
443
  collector.waitForUpdate(timeoutMs = 5000)
444
+ val countBefore = collector.events.size
422
445
 
423
- // Only paginate if we don't have enough items yet
424
- if (collector.events.size < requestedLimit) {
425
- val hitStart = timeline.paginateBackwards(requestedLimit.toUShort())
446
+ // Reset cursor on initial load
447
+ if (!isPagination) {
448
+ oldestReturnedEventId.remove(roomId)
449
+ }
450
+
451
+ // Paginate when: first load with too few items, OR explicit pagination request
452
+ if (isPagination || countBefore < requestedLimit) {
453
+ hitStart = timeline.paginateBackwards(requestedLimit.toUShort())
426
454
  if (!hitStart) {
427
455
  collector.waitForUpdate(timeoutMs = 5000)
428
456
  }
@@ -432,7 +460,28 @@ class MatrixSDKBridge(private val context: Context) {
432
460
  paginatingRooms.remove(roomId)
433
461
  }
434
462
 
435
- var events = collector.events.takeLast(requestedLimit).map { it.toMutableMap() }
463
+ val allEvents = collector.events
464
+ val cursorId = oldestReturnedEventId[roomId]
465
+
466
+ var events = if (isPagination && cursorId != null) {
467
+ // Pagination: find the cursor event and return events before it
468
+ val cursorIdx = allEvents.indexOfFirst { (it["eventId"] as? String) == cursorId }
469
+ if (cursorIdx > 0) {
470
+ allEvents.take(cursorIdx).takeLast(requestedLimit).map { it.toMutableMap() }
471
+ } else {
472
+ emptyList<MutableMap<String, Any?>>()
473
+ }
474
+ } else {
475
+ // Initial load: return newest events
476
+ allEvents.takeLast(requestedLimit).map { it.toMutableMap() }
477
+ }
478
+
479
+ // Update cursor to the oldest event we're returning
480
+ events.firstOrNull()?.let { oldest ->
481
+ (oldest["eventId"] as? String)?.let { eid ->
482
+ oldestReturnedEventId[roomId] = eid
483
+ }
484
+ }
436
485
 
437
486
  // Apply receipt watermark: if any own event has readBy data,
438
487
  // all earlier own events in the timeline are also read.
@@ -466,9 +515,15 @@ class MatrixSDKBridge(private val context: Context) {
466
515
  }
467
516
  }
468
517
 
518
+ // Return a pagination token so the JS layer knows more messages are available.
519
+ // The Rust SDK timeline handles pagination state internally, so we use a
520
+ // synthetic token ("more") to signal that further back-pagination is possible.
521
+ // Also stop if pagination returned no new events (timeline fully loaded).
522
+ val nextBatch: String? = if (hitStart || events.isEmpty()) null else "more"
523
+
469
524
  return mapOf(
470
525
  "events" to events,
471
- "nextBatch" to null,
526
+ "nextBatch" to nextBatch,
472
527
  )
473
528
  }
474
529
 
@@ -709,6 +764,20 @@ class MatrixSDKBridge(private val context: Context) {
709
764
  c.encryption().waitForE2eeInitializationTasks()
710
765
  }
711
766
 
767
+ /**
768
+ * Cross-signs the given device using the current cross-signing keys.
769
+ * After recoverAndSetup (which calls recover + waitForE2eeInitializationTasks),
770
+ * the SDK should already have cross-signed this device. This method ensures
771
+ * the E2EE initialization is complete and then resolves.
772
+ */
773
+ suspend fun verifyDevice(deviceId: String) {
774
+ val c = requireClient()
775
+ val enc = c.encryption()
776
+ // Ensure cross-signing keys are fully imported and the device is signed
777
+ enc.waitForE2eeInitializationTasks()
778
+ android.util.Log.d("CapMatrix", "verifyDevice($deviceId) — verificationState: ${enc.verificationState()}")
779
+ }
780
+
712
781
  suspend fun getEncryptionStatus(): Map<String, Any?> {
713
782
  val c = requireClient()
714
783
  val enc = c.encryption()
@@ -720,6 +789,15 @@ class MatrixSDKBridge(private val context: Context) {
720
789
  val verificationState = enc.verificationState()
721
790
  val isVerified = verificationState == VerificationState.VERIFIED
722
791
 
792
+ // recoveryState reflects the LOCAL device's state (.DISABLED on a returning
793
+ // device that hasn't recovered yet). To decide whether encryption was
794
+ // set up server-side we also check if a backup exists on the server.
795
+ val ssReady = if (recoveryState == RecoveryState.ENABLED) {
796
+ true
797
+ } else {
798
+ try { enc.backupExistsOnServer() } catch (_: Exception) { false }
799
+ }
800
+
723
801
  return mapOf(
724
802
  "isCrossSigningReady" to isVerified,
725
803
  "crossSigningStatus" to mapOf(
@@ -729,7 +807,7 @@ class MatrixSDKBridge(private val context: Context) {
729
807
  "isReady" to isVerified,
730
808
  ),
731
809
  "isKeyBackupEnabled" to isBackupEnabled,
732
- "isSecretStorageReady" to (recoveryState == RecoveryState.ENABLED),
810
+ "isSecretStorageReady" to ssReady,
733
811
  )
734
812
  }
735
813
 
@@ -798,10 +876,103 @@ class MatrixSDKBridge(private val context: Context) {
798
876
  return c.encryption().recoveryState() == RecoveryState.ENABLED
799
877
  }
800
878
 
801
- suspend fun recoverAndSetup(recoveryKey: String) {
879
+ suspend fun recoverAndSetup(recoveryKey: String?, passphrase: String?) {
802
880
  val c = requireClient()
803
- c.encryption().recover(recoveryKey)
881
+ val key = recoveryKey ?: passphrase?.let { deriveRecoveryKeyFromPassphrase(c, it) }
882
+ ?: throw IllegalArgumentException("recoveryKey or passphrase required")
883
+
884
+ val enc = c.encryption()
885
+ enc.recover(key)
886
+
887
+ // Wait for the SDK to finish importing cross-signing keys and
888
+ // verifying the current device after recovery.
889
+ enc.waitForE2eeInitializationTasks()
890
+
891
+ // Enable key backup if not already active
892
+ if (enc.backupState() != BackupState.ENABLED) {
893
+ try { enc.enableBackups() } catch (_: Exception) { }
894
+ }
895
+ }
896
+
897
+ // region Passphrase → recovery key derivation
898
+
899
+ /**
900
+ * Derive a Matrix recovery key from a passphrase using PBKDF2 params
901
+ * stored in the account's secret storage.
902
+ */
903
+ private suspend fun deriveRecoveryKeyFromPassphrase(client: Client, passphrase: String): String {
904
+ // 1. Get the default key ID
905
+ val defaultKeyJson = client.accountData("m.secret_storage.default_key")
906
+ ?: throw IllegalStateException("No default secret storage key found")
907
+ val defaultKeyMap = org.json.JSONObject(defaultKeyJson)
908
+ val keyId = defaultKeyMap.getString("key")
909
+
910
+ // 2. Get the key info with PBKDF2 params
911
+ val keyInfoJson = client.accountData("m.secret_storage.key.$keyId")
912
+ ?: throw IllegalStateException("Secret storage key info not found for $keyId")
913
+ val keyInfoMap = org.json.JSONObject(keyInfoJson)
914
+ val ppObj = keyInfoMap.optJSONObject("passphrase")
915
+ ?: throw IllegalStateException("Secret storage key has no passphrase params — use recovery key instead")
916
+ val salt = ppObj.getString("salt")
917
+ val iterations = ppObj.getInt("iterations")
918
+ val bits = ppObj.optInt("bits", 256)
919
+
920
+ // 3. PBKDF2-SHA-512 derivation
921
+ val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
922
+ val spec = javax.crypto.spec.PBEKeySpec(
923
+ passphrase.toCharArray(),
924
+ salt.toByteArray(Charsets.UTF_8),
925
+ iterations,
926
+ bits,
927
+ )
928
+ val derivedBytes = factory.generateSecret(spec).encoded
929
+
930
+ // 4. Encode as Matrix recovery key
931
+ return encodeRecoveryKey(derivedBytes)
932
+ }
933
+
934
+ /**
935
+ * Encode raw key bytes as a Matrix recovery key (base58 with 0x8b01 prefix + parity byte).
936
+ */
937
+ private fun encodeRecoveryKey(keyData: ByteArray): String {
938
+ val prefix = byteArrayOf(0x8b.toByte(), 0x01)
939
+ val buf = prefix + keyData
940
+ var parity: Byte = 0
941
+ for (b in buf) parity = (parity.toInt() xor b.toInt()).toByte()
942
+ val full = buf + byteArrayOf(parity)
943
+ val encoded = base58Encode(full)
944
+ return encoded.chunked(4).joinToString(" ")
945
+ }
946
+
947
+ private val base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray()
948
+
949
+ private fun base58Encode(data: ByteArray): String {
950
+ var bytes = data.toMutableList()
951
+ val result = mutableListOf<Char>()
952
+
953
+ while (bytes.isNotEmpty()) {
954
+ var carry = 0
955
+ val newBytes = mutableListOf<Byte>()
956
+ for (b in bytes) {
957
+ carry = carry * 256 + (b.toInt() and 0xFF)
958
+ if (newBytes.isNotEmpty() || carry / 58 > 0) {
959
+ newBytes.add((carry / 58).toByte())
960
+ }
961
+ carry %= 58
962
+ }
963
+ result.add(base58Alphabet[carry])
964
+ bytes = newBytes
965
+ }
966
+
967
+ // Preserve leading zeros
968
+ for (b in data) {
969
+ if (b.toInt() != 0) break
970
+ result.add(base58Alphabet[0])
971
+ }
972
+
973
+ return result.reversed().joinToString("")
804
974
  }
975
+ // endregion
805
976
 
806
977
  suspend fun resetRecoveryKey(): Map<String, Any?> {
807
978
  val c = requireClient()
@@ -514,6 +514,20 @@ class MatrixPlugin : Plugin() {
514
514
  }
515
515
  }
516
516
 
517
+ @PluginMethod
518
+ fun verifyDevice(call: PluginCall) {
519
+ val deviceId = call.getString("deviceId") ?: return call.reject("Missing deviceId")
520
+
521
+ scope.launch {
522
+ try {
523
+ bridge.verifyDevice(deviceId)
524
+ call.resolve()
525
+ } catch (e: Exception) {
526
+ call.reject(e.message ?: "verifyDevice failed", e)
527
+ }
528
+ }
529
+ }
530
+
517
531
  @PluginMethod
518
532
  fun setPusher(call: PluginCall) {
519
533
  call.reject("setPusher is not yet supported on this platform")
@@ -733,12 +747,16 @@ class MatrixPlugin : Plugin() {
733
747
 
734
748
  @PluginMethod
735
749
  fun recoverAndSetup(call: PluginCall) {
736
- val recoveryKey = call.getString("recoveryKey") ?: call.getString("passphrase")
737
- ?: return call.reject("Missing recoveryKey or passphrase")
750
+ val recoveryKey = call.getString("recoveryKey")
751
+ val passphrase = call.getString("passphrase")
752
+
753
+ if (recoveryKey == null && passphrase == null) {
754
+ return call.reject("Missing recoveryKey or passphrase")
755
+ }
738
756
 
739
757
  scope.launch {
740
758
  try {
741
- bridge.recoverAndSetup(recoveryKey)
759
+ bridge.recoverAndSetup(recoveryKey = recoveryKey, passphrase = passphrase)
742
760
  call.resolve()
743
761
  } catch (e: Exception) {
744
762
  call.reject(e.message ?: "recoverAndSetup failed", e)
package/dist/docs.json CHANGED
@@ -1466,6 +1466,13 @@
1466
1466
  "complexTypes": [],
1467
1467
  "type": "string"
1468
1468
  },
1469
+ {
1470
+ "name": "stateKey",
1471
+ "tags": [],
1472
+ "docs": "State key for state events (e.g. target user ID for m.room.member)",
1473
+ "complexTypes": [],
1474
+ "type": "string | undefined"
1475
+ },
1469
1476
  {
1470
1477
  "name": "content",
1471
1478
  "tags": [],
@@ -104,6 +104,8 @@ export interface MatrixEvent {
104
104
  roomId: string;
105
105
  senderId: string;
106
106
  type: string;
107
+ /** State key for state events (e.g. target user ID for m.room.member) */
108
+ stateKey?: string;
107
109
  content: Record<string, unknown>;
108
110
  originServerTs: number;
109
111
  /** Delivery/read status for own messages: 'sending' | 'sent' | 'delivered' | 'read' */
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// Auth & Session\n\nexport interface LoginOptions {\n homeserverUrl: string;\n userId: string;\n password: string;\n}\n\nexport interface LoginWithTokenOptions {\n homeserverUrl: string;\n accessToken: string;\n userId: string;\n deviceId: string;\n}\n\nexport interface SessionInfo {\n accessToken: string;\n userId: string;\n deviceId: string;\n homeserverUrl: string;\n}\n\n// Messaging\n\nexport interface SendMessageOptions {\n roomId: string;\n body: string;\n msgtype?: 'm.text' | 'm.notice' | 'm.emote' | 'm.image' | 'm.audio' | 'm.video' | 'm.file';\n fileUri?: string;\n fileName?: string;\n mimeType?: string;\n fileSize?: number;\n /** Audio/video duration in milliseconds (sets info.duration per Matrix spec) */\n duration?: number;\n /** Image/video width in pixels (sets info.w per Matrix spec) */\n width?: number;\n /** Image/video height in pixels (sets info.h per Matrix spec) */\n height?: number;\n}\n\n// Presence\n\nexport interface PresenceInfo {\n presence: 'online' | 'offline' | 'unavailable';\n statusMsg?: string;\n lastActiveAgo?: number;\n}\n\n// Typing\n\nexport interface TypingEvent {\n roomId: string;\n userIds: string[];\n}\n\nexport interface ReceiptReceivedEvent {\n roomId: string;\n /** The event that was read */\n eventId: string;\n /** The user who sent the read receipt */\n userId: string;\n}\n\nexport interface PresenceChangedEvent {\n userId: string;\n presence: PresenceInfo;\n}\n\n// Edit & Reply\n\nexport interface EditMessageOptions {\n roomId: string;\n eventId: string;\n newBody: string;\n /** Required when editing a media message; must match the original msgtype */\n msgtype?: 'm.text' | 'm.notice' | 'm.emote' | 'm.image' | 'm.audio' | 'm.video' | 'm.file';\n /** New file to replace the media content (optional for caption-only edits) */\n fileUri?: string;\n fileName?: string;\n mimeType?: string;\n fileSize?: number;\n /** Audio/video duration in milliseconds */\n duration?: number;\n /** Image/video width in pixels */\n width?: number;\n /** Image/video height in pixels */\n height?: number;\n}\n\nexport interface SendReplyOptions {\n roomId: string;\n body: string;\n replyToEventId: string;\n msgtype?: 'm.text' | 'm.notice' | 'm.emote' | 'm.image' | 'm.audio' | 'm.video' | 'm.file';\n fileUri?: string;\n fileName?: string;\n mimeType?: string;\n fileSize?: number;\n /** Audio/video duration in milliseconds (sets info.duration per Matrix spec) */\n duration?: number;\n /** Image/video width in pixels (sets info.w per Matrix spec) */\n width?: number;\n /** Image/video height in pixels (sets info.h per Matrix spec) */\n height?: number;\n}\n\n// Upload\n\nexport interface UploadContentOptions {\n fileUri: string;\n fileName: string;\n mimeType: string;\n}\n\nexport interface UploadContentResult {\n contentUri: string;\n}\n\n// Thumbnail\n\nexport interface ThumbnailUrlOptions {\n mxcUrl: string;\n width: number;\n height: number;\n method?: 'scale' | 'crop';\n}\n\nexport interface MatrixEvent {\n eventId: string;\n roomId: string;\n senderId: string;\n type: string;\n content: Record<string, unknown>;\n originServerTs: number;\n /** Delivery/read status for own messages: 'sending' | 'sent' | 'delivered' | 'read' */\n status?: 'sending' | 'sent' | 'delivered' | 'read';\n /** User IDs that have read this event */\n readBy?: string[];\n /** Unsigned data (e.g. m.relations for edits, transaction_id for local echo) */\n unsigned?: Record<string, unknown>;\n}\n\n// Rooms\n\nexport interface RoomSummary {\n roomId: string;\n name: string;\n topic?: string;\n memberCount: number;\n isEncrypted: boolean;\n unreadCount: number;\n lastEventTs?: number;\n membership?: 'join' | 'invite' | 'leave' | 'ban';\n avatarUrl?: string;\n isDirect?: boolean;\n}\n\nexport interface RoomMember {\n userId: string;\n displayName?: string;\n membership: 'join' | 'invite' | 'leave' | 'ban';\n avatarUrl?: string;\n}\n\n// Device Management\n\nexport interface DeviceInfo {\n deviceId: string;\n displayName?: string;\n lastSeenTs?: number;\n lastSeenIp?: string;\n /** Whether this device is verified via cross-signing */\n isCrossSigningVerified?: boolean;\n}\n\n// Pusher\n\nexport interface PusherOptions {\n pushkey: string;\n kind: string | null;\n appId: string;\n appDisplayName: string;\n deviceDisplayName: string;\n lang: string;\n data: { url: string; format?: string };\n}\n\n// User Discovery\n\nexport interface UserProfile {\n userId: string;\n displayName?: string;\n avatarUrl?: string;\n}\n\n// Encryption\n\nexport interface CrossSigningStatus {\n hasMaster: boolean;\n hasSelfSigning: boolean;\n hasUserSigning: boolean;\n isReady: boolean;\n}\n\nexport interface KeyBackupStatus {\n exists: boolean;\n version?: string;\n enabled: boolean;\n}\n\nexport interface RecoveryKeyInfo {\n recoveryKey: string;\n}\n\nexport interface EncryptionStatus {\n isCrossSigningReady: boolean;\n crossSigningStatus: CrossSigningStatus;\n isKeyBackupEnabled: boolean;\n keyBackupVersion?: string;\n isSecretStorageReady: boolean;\n}\n\n// Events & Sync\n\nexport type SyncState = 'INITIAL' | 'SYNCING' | 'ERROR' | 'STOPPED';\n\nexport interface SyncStateChangeEvent {\n state: SyncState;\n error?: string;\n}\n\nexport interface MessageReceivedEvent {\n event: MatrixEvent;\n}\n\nexport interface RoomUpdatedEvent {\n roomId: string;\n summary: RoomSummary;\n}\n\n// Plugin Interface\n\nexport interface MatrixPlugin {\n // Auth\n login(options: LoginOptions): Promise<SessionInfo>;\n loginWithToken(options: LoginWithTokenOptions): Promise<SessionInfo>;\n logout(): Promise<void>;\n getSession(): Promise<SessionInfo | null>;\n\n // Sync\n startSync(): Promise<void>;\n stopSync(): Promise<void>;\n getSyncState(): Promise<{ state: SyncState }>;\n\n // Rooms\n createRoom(options: {\n name?: string;\n topic?: string;\n isEncrypted?: boolean;\n isDirect?: boolean;\n invite?: string[];\n preset?: 'private_chat' | 'trusted_private_chat' | 'public_chat';\n historyVisibility?: 'invited' | 'joined' | 'shared' | 'world_readable';\n }): Promise<{ roomId: string }>;\n getRooms(): Promise<{ rooms: RoomSummary[] }>;\n getRoomMembers(options: { roomId: string }): Promise<{ members: RoomMember[] }>;\n joinRoom(options: { roomIdOrAlias: string }): Promise<{ roomId: string }>;\n leaveRoom(options: { roomId: string }): Promise<void>;\n forgetRoom(options: { roomId: string }): Promise<void>;\n\n // Messaging\n sendMessage(options: SendMessageOptions): Promise<{ eventId: string }>;\n editMessage(options: EditMessageOptions): Promise<{ eventId: string }>;\n sendReply(options: SendReplyOptions): Promise<{ eventId: string }>;\n getRoomMessages(options: {\n roomId: string;\n limit?: number;\n from?: string;\n }): Promise<{ events: MatrixEvent[]; nextBatch?: string }>;\n markRoomAsRead(options: {\n roomId: string;\n eventId: string;\n }): Promise<void>;\n refreshEventStatuses(options: {\n roomId: string;\n eventIds: string[];\n }): Promise<{ events: MatrixEvent[] }>;\n redactEvent(options: {\n roomId: string;\n eventId: string;\n reason?: string;\n }): Promise<void>;\n sendReaction(options: {\n roomId: string;\n eventId: string;\n key: string;\n }): Promise<{ eventId: string }>;\n\n // Room Management\n setRoomName(options: { roomId: string; name: string }): Promise<void>;\n setRoomTopic(options: { roomId: string; topic: string }): Promise<void>;\n setRoomAvatar(options: { roomId: string; mxcUrl: string }): Promise<void>;\n inviteUser(options: { roomId: string; userId: string }): Promise<void>;\n kickUser(options: { roomId: string; userId: string; reason?: string }): Promise<void>;\n banUser(options: { roomId: string; userId: string; reason?: string }): Promise<void>;\n unbanUser(options: { roomId: string; userId: string }): Promise<void>;\n\n // Typing\n sendTyping(options: {\n roomId: string;\n isTyping: boolean;\n timeout?: number;\n }): Promise<void>;\n\n // Media\n getMediaUrl(options: { mxcUrl: string }): Promise<{ httpUrl: string }>;\n getThumbnailUrl(options: ThumbnailUrlOptions): Promise<{ httpUrl: string }>;\n uploadContent(options: UploadContentOptions): Promise<UploadContentResult>;\n\n // User Discovery\n searchUsers(options: {\n searchTerm: string;\n limit?: number;\n }): Promise<{ results: UserProfile[]; limited: boolean }>;\n\n // Presence\n setPresence(options: {\n presence: 'online' | 'offline' | 'unavailable';\n statusMsg?: string;\n }): Promise<void>;\n getPresence(options: { userId: string }): Promise<PresenceInfo>;\n\n // Device Management\n getDevices(): Promise<{ devices: DeviceInfo[] }>;\n deleteDevice(options: { deviceId: string; auth?: Record<string, unknown> }): Promise<void>;\n verifyDevice(options: { deviceId: string }): Promise<void>;\n\n // Push\n setPusher(options: PusherOptions): Promise<void>;\n\n // Encryption\n initializeCrypto(): Promise<void>;\n getEncryptionStatus(): Promise<EncryptionStatus>;\n bootstrapCrossSigning(): Promise<void>;\n setupKeyBackup(): Promise<KeyBackupStatus>;\n getKeyBackupStatus(): Promise<KeyBackupStatus>;\n restoreKeyBackup(options?: {\n recoveryKey?: string;\n }): Promise<{ importedKeys: number }>;\n setupRecovery(options?: {\n passphrase?: string;\n /**\n * Passphrase for the *existing* secret storage key, used by\n * bootstrapSecretStorage to decrypt and migrate the current cross-signing\n * and backup secrets into the newly created SSSS. Only needed on web;\n * native platforms (Rust SDK) handle the migration internally.\n */\n existingPassphrase?: string;\n }): Promise<RecoveryKeyInfo>;\n /** Wipe all local Matrix state (crypto DB, session, caches). */\n clearAllData(): Promise<void>;\n isRecoveryEnabled(): Promise<{ enabled: boolean }>;\n recoverAndSetup(options: {\n recoveryKey?: string;\n passphrase?: string;\n }): Promise<void>;\n resetRecoveryKey(options?: {\n passphrase?: string;\n }): Promise<RecoveryKeyInfo>;\n exportRoomKeys(options: {\n passphrase: string;\n }): Promise<{ data: string }>;\n importRoomKeys(options: {\n data: string;\n passphrase: string;\n }): Promise<{ importedKeys: number }>;\n\n // Listeners\n addListener(\n event: 'syncStateChange',\n listenerFunc: (data: SyncStateChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'messageReceived',\n listenerFunc: (data: MessageReceivedEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'roomUpdated',\n listenerFunc: (data: RoomUpdatedEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'typingChanged',\n listenerFunc: (data: TypingEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'receiptReceived',\n listenerFunc: (data: ReceiptReceivedEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'presenceChanged',\n listenerFunc: (data: PresenceChangedEvent) => void,\n ): Promise<PluginListenerHandle>;\n removeAllListeners(): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// Auth & Session\n\nexport interface LoginOptions {\n homeserverUrl: string;\n userId: string;\n password: string;\n}\n\nexport interface LoginWithTokenOptions {\n homeserverUrl: string;\n accessToken: string;\n userId: string;\n deviceId: string;\n}\n\nexport interface SessionInfo {\n accessToken: string;\n userId: string;\n deviceId: string;\n homeserverUrl: string;\n}\n\n// Messaging\n\nexport interface SendMessageOptions {\n roomId: string;\n body: string;\n msgtype?: 'm.text' | 'm.notice' | 'm.emote' | 'm.image' | 'm.audio' | 'm.video' | 'm.file';\n fileUri?: string;\n fileName?: string;\n mimeType?: string;\n fileSize?: number;\n /** Audio/video duration in milliseconds (sets info.duration per Matrix spec) */\n duration?: number;\n /** Image/video width in pixels (sets info.w per Matrix spec) */\n width?: number;\n /** Image/video height in pixels (sets info.h per Matrix spec) */\n height?: number;\n}\n\n// Presence\n\nexport interface PresenceInfo {\n presence: 'online' | 'offline' | 'unavailable';\n statusMsg?: string;\n lastActiveAgo?: number;\n}\n\n// Typing\n\nexport interface TypingEvent {\n roomId: string;\n userIds: string[];\n}\n\nexport interface ReceiptReceivedEvent {\n roomId: string;\n /** The event that was read */\n eventId: string;\n /** The user who sent the read receipt */\n userId: string;\n}\n\nexport interface PresenceChangedEvent {\n userId: string;\n presence: PresenceInfo;\n}\n\n// Edit & Reply\n\nexport interface EditMessageOptions {\n roomId: string;\n eventId: string;\n newBody: string;\n /** Required when editing a media message; must match the original msgtype */\n msgtype?: 'm.text' | 'm.notice' | 'm.emote' | 'm.image' | 'm.audio' | 'm.video' | 'm.file';\n /** New file to replace the media content (optional for caption-only edits) */\n fileUri?: string;\n fileName?: string;\n mimeType?: string;\n fileSize?: number;\n /** Audio/video duration in milliseconds */\n duration?: number;\n /** Image/video width in pixels */\n width?: number;\n /** Image/video height in pixels */\n height?: number;\n}\n\nexport interface SendReplyOptions {\n roomId: string;\n body: string;\n replyToEventId: string;\n msgtype?: 'm.text' | 'm.notice' | 'm.emote' | 'm.image' | 'm.audio' | 'm.video' | 'm.file';\n fileUri?: string;\n fileName?: string;\n mimeType?: string;\n fileSize?: number;\n /** Audio/video duration in milliseconds (sets info.duration per Matrix spec) */\n duration?: number;\n /** Image/video width in pixels (sets info.w per Matrix spec) */\n width?: number;\n /** Image/video height in pixels (sets info.h per Matrix spec) */\n height?: number;\n}\n\n// Upload\n\nexport interface UploadContentOptions {\n fileUri: string;\n fileName: string;\n mimeType: string;\n}\n\nexport interface UploadContentResult {\n contentUri: string;\n}\n\n// Thumbnail\n\nexport interface ThumbnailUrlOptions {\n mxcUrl: string;\n width: number;\n height: number;\n method?: 'scale' | 'crop';\n}\n\nexport interface MatrixEvent {\n eventId: string;\n roomId: string;\n senderId: string;\n type: string;\n /** State key for state events (e.g. target user ID for m.room.member) */\n stateKey?: string;\n content: Record<string, unknown>;\n originServerTs: number;\n /** Delivery/read status for own messages: 'sending' | 'sent' | 'delivered' | 'read' */\n status?: 'sending' | 'sent' | 'delivered' | 'read';\n /** User IDs that have read this event */\n readBy?: string[];\n /** Unsigned data (e.g. m.relations for edits, transaction_id for local echo) */\n unsigned?: Record<string, unknown>;\n}\n\n// Rooms\n\nexport interface RoomSummary {\n roomId: string;\n name: string;\n topic?: string;\n memberCount: number;\n isEncrypted: boolean;\n unreadCount: number;\n lastEventTs?: number;\n membership?: 'join' | 'invite' | 'leave' | 'ban';\n avatarUrl?: string;\n isDirect?: boolean;\n}\n\nexport interface RoomMember {\n userId: string;\n displayName?: string;\n membership: 'join' | 'invite' | 'leave' | 'ban';\n avatarUrl?: string;\n}\n\n// Device Management\n\nexport interface DeviceInfo {\n deviceId: string;\n displayName?: string;\n lastSeenTs?: number;\n lastSeenIp?: string;\n /** Whether this device is verified via cross-signing */\n isCrossSigningVerified?: boolean;\n}\n\n// Pusher\n\nexport interface PusherOptions {\n pushkey: string;\n kind: string | null;\n appId: string;\n appDisplayName: string;\n deviceDisplayName: string;\n lang: string;\n data: { url: string; format?: string };\n}\n\n// User Discovery\n\nexport interface UserProfile {\n userId: string;\n displayName?: string;\n avatarUrl?: string;\n}\n\n// Encryption\n\nexport interface CrossSigningStatus {\n hasMaster: boolean;\n hasSelfSigning: boolean;\n hasUserSigning: boolean;\n isReady: boolean;\n}\n\nexport interface KeyBackupStatus {\n exists: boolean;\n version?: string;\n enabled: boolean;\n}\n\nexport interface RecoveryKeyInfo {\n recoveryKey: string;\n}\n\nexport interface EncryptionStatus {\n isCrossSigningReady: boolean;\n crossSigningStatus: CrossSigningStatus;\n isKeyBackupEnabled: boolean;\n keyBackupVersion?: string;\n isSecretStorageReady: boolean;\n}\n\n// Events & Sync\n\nexport type SyncState = 'INITIAL' | 'SYNCING' | 'ERROR' | 'STOPPED';\n\nexport interface SyncStateChangeEvent {\n state: SyncState;\n error?: string;\n}\n\nexport interface MessageReceivedEvent {\n event: MatrixEvent;\n}\n\nexport interface RoomUpdatedEvent {\n roomId: string;\n summary: RoomSummary;\n}\n\n// Plugin Interface\n\nexport interface MatrixPlugin {\n // Auth\n login(options: LoginOptions): Promise<SessionInfo>;\n loginWithToken(options: LoginWithTokenOptions): Promise<SessionInfo>;\n logout(): Promise<void>;\n getSession(): Promise<SessionInfo | null>;\n\n // Sync\n startSync(): Promise<void>;\n stopSync(): Promise<void>;\n getSyncState(): Promise<{ state: SyncState }>;\n\n // Rooms\n createRoom(options: {\n name?: string;\n topic?: string;\n isEncrypted?: boolean;\n isDirect?: boolean;\n invite?: string[];\n preset?: 'private_chat' | 'trusted_private_chat' | 'public_chat';\n historyVisibility?: 'invited' | 'joined' | 'shared' | 'world_readable';\n }): Promise<{ roomId: string }>;\n getRooms(): Promise<{ rooms: RoomSummary[] }>;\n getRoomMembers(options: { roomId: string }): Promise<{ members: RoomMember[] }>;\n joinRoom(options: { roomIdOrAlias: string }): Promise<{ roomId: string }>;\n leaveRoom(options: { roomId: string }): Promise<void>;\n forgetRoom(options: { roomId: string }): Promise<void>;\n\n // Messaging\n sendMessage(options: SendMessageOptions): Promise<{ eventId: string }>;\n editMessage(options: EditMessageOptions): Promise<{ eventId: string }>;\n sendReply(options: SendReplyOptions): Promise<{ eventId: string }>;\n getRoomMessages(options: {\n roomId: string;\n limit?: number;\n from?: string;\n }): Promise<{ events: MatrixEvent[]; nextBatch?: string }>;\n markRoomAsRead(options: {\n roomId: string;\n eventId: string;\n }): Promise<void>;\n refreshEventStatuses(options: {\n roomId: string;\n eventIds: string[];\n }): Promise<{ events: MatrixEvent[] }>;\n redactEvent(options: {\n roomId: string;\n eventId: string;\n reason?: string;\n }): Promise<void>;\n sendReaction(options: {\n roomId: string;\n eventId: string;\n key: string;\n }): Promise<{ eventId: string }>;\n\n // Room Management\n setRoomName(options: { roomId: string; name: string }): Promise<void>;\n setRoomTopic(options: { roomId: string; topic: string }): Promise<void>;\n setRoomAvatar(options: { roomId: string; mxcUrl: string }): Promise<void>;\n inviteUser(options: { roomId: string; userId: string }): Promise<void>;\n kickUser(options: { roomId: string; userId: string; reason?: string }): Promise<void>;\n banUser(options: { roomId: string; userId: string; reason?: string }): Promise<void>;\n unbanUser(options: { roomId: string; userId: string }): Promise<void>;\n\n // Typing\n sendTyping(options: {\n roomId: string;\n isTyping: boolean;\n timeout?: number;\n }): Promise<void>;\n\n // Media\n getMediaUrl(options: { mxcUrl: string }): Promise<{ httpUrl: string }>;\n getThumbnailUrl(options: ThumbnailUrlOptions): Promise<{ httpUrl: string }>;\n uploadContent(options: UploadContentOptions): Promise<UploadContentResult>;\n\n // User Discovery\n searchUsers(options: {\n searchTerm: string;\n limit?: number;\n }): Promise<{ results: UserProfile[]; limited: boolean }>;\n\n // Presence\n setPresence(options: {\n presence: 'online' | 'offline' | 'unavailable';\n statusMsg?: string;\n }): Promise<void>;\n getPresence(options: { userId: string }): Promise<PresenceInfo>;\n\n // Device Management\n getDevices(): Promise<{ devices: DeviceInfo[] }>;\n deleteDevice(options: { deviceId: string; auth?: Record<string, unknown> }): Promise<void>;\n verifyDevice(options: { deviceId: string }): Promise<void>;\n\n // Push\n setPusher(options: PusherOptions): Promise<void>;\n\n // Encryption\n initializeCrypto(): Promise<void>;\n getEncryptionStatus(): Promise<EncryptionStatus>;\n bootstrapCrossSigning(): Promise<void>;\n setupKeyBackup(): Promise<KeyBackupStatus>;\n getKeyBackupStatus(): Promise<KeyBackupStatus>;\n restoreKeyBackup(options?: {\n recoveryKey?: string;\n }): Promise<{ importedKeys: number }>;\n setupRecovery(options?: {\n passphrase?: string;\n /**\n * Passphrase for the *existing* secret storage key, used by\n * bootstrapSecretStorage to decrypt and migrate the current cross-signing\n * and backup secrets into the newly created SSSS. Only needed on web;\n * native platforms (Rust SDK) handle the migration internally.\n */\n existingPassphrase?: string;\n }): Promise<RecoveryKeyInfo>;\n /** Wipe all local Matrix state (crypto DB, session, caches). */\n clearAllData(): Promise<void>;\n isRecoveryEnabled(): Promise<{ enabled: boolean }>;\n recoverAndSetup(options: {\n recoveryKey?: string;\n passphrase?: string;\n }): Promise<void>;\n resetRecoveryKey(options?: {\n passphrase?: string;\n }): Promise<RecoveryKeyInfo>;\n exportRoomKeys(options: {\n passphrase: string;\n }): Promise<{ data: string }>;\n importRoomKeys(options: {\n data: string;\n passphrase: string;\n }): Promise<{ importedKeys: number }>;\n\n // Listeners\n addListener(\n event: 'syncStateChange',\n listenerFunc: (data: SyncStateChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'messageReceived',\n listenerFunc: (data: MessageReceivedEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'roomUpdated',\n listenerFunc: (data: RoomUpdatedEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'typingChanged',\n listenerFunc: (data: TypingEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'receiptReceived',\n listenerFunc: (data: ReceiptReceivedEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n event: 'presenceChanged',\n listenerFunc: (data: PresenceChangedEvent) => void,\n ): Promise<PluginListenerHandle>;\n removeAllListeners(): Promise<void>;\n}\n"]}
package/dist/esm/web.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
+ import { initAsync as initCryptoWasm } from '@matrix-org/matrix-sdk-crypto-wasm';
2
3
  import { createClient, ClientEvent, RoomEvent, RoomMemberEvent, Direction, MsgType, EventType, RelationType, UserEvent, } from 'matrix-js-sdk';
3
- import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api/recovery-key';
4
4
  import { deriveRecoveryKeyFromPassphrase } from 'matrix-js-sdk/lib/crypto-api/key-passphrase';
5
+ import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api/recovery-key';
5
6
  const SESSION_KEY = 'matrix_session';
6
7
  export class MatrixWeb extends WebPlugin {
7
8
  constructor() {
@@ -137,8 +138,11 @@ export class MatrixWeb extends WebPlugin {
137
138
  error: (_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message,
138
139
  });
139
140
  });
140
- this.client.on(RoomEvent.Timeline, (event, room) => {
141
+ this.client.on(RoomEvent.Timeline, (event, room, toStartOfTimeline) => {
141
142
  var _a;
143
+ // Skip back-paginated events — they're loaded via getRoomMessages.
144
+ if (toStartOfTimeline)
145
+ return;
142
146
  this.notifyListeners('messageReceived', {
143
147
  event: this.serializeEvent(event, room === null || room === void 0 ? void 0 : room.roomId),
144
148
  });
@@ -444,7 +448,6 @@ export class MatrixWeb extends WebPlugin {
444
448
  return t !== EventType.Reaction && t !== EventType.RoomRedaction;
445
449
  });
446
450
  const events = displayableEvents
447
- .slice(-limit)
448
451
  .map((e) => this.serializeEvent(e, options.roomId))
449
452
  .sort((a, b) => a.originServerTs - b.originServerTs);
450
453
  const backToken = (_b = timeline.getPaginationToken(Direction.Backward)) !== null && _b !== void 0 ? _b : undefined;
@@ -666,6 +669,18 @@ export class MatrixWeb extends WebPlugin {
666
669
  async initializeCrypto() {
667
670
  var _a, _b;
668
671
  this.requireClient();
672
+ // Pre-initialize the WASM module with a root-relative URL before
673
+ // matrix-js-sdk tries with a URL relative to the bundled chunk
674
+ // (which breaks in bundlers like esbuild/Vite that relocate modules).
675
+ // The host app must serve the WASM file at /matrix_sdk_crypto_wasm_bg.wasm
676
+ // (e.g. via an asset copy in angular.json / project.json).
677
+ // initAsync's internal guard ensures this is a no-op if already loaded.
678
+ try {
679
+ await initCryptoWasm(new URL('/matrix_sdk_crypto_wasm_bg.wasm', window.location.origin));
680
+ }
681
+ catch (e) {
682
+ console.warn('[CapMatrix] WASM pre-init failed, falling back to default URL:', e);
683
+ }
669
684
  const cryptoOpts = { cryptoDatabasePrefix: 'matrix-js-sdk' };
670
685
  try {
671
686
  await this.client.initRustCrypto(cryptoOpts);
@@ -804,16 +819,11 @@ export class MatrixWeb extends WebPlugin {
804
819
  this.fallbackPassphrase = options.existingPassphrase;
805
820
  }
806
821
  try {
807
- const bootstrapPromise = crypto.bootstrapSecretStorage({
822
+ await crypto.bootstrapSecretStorage({
808
823
  createSecretStorageKey: async () => keyInfo,
809
824
  setupNewSecretStorage: true,
810
825
  setupNewKeyBackup: true,
811
826
  });
812
- // Guard against SDK hanging when it can't retrieve the old SSSS key
813
- const timeoutPromise = new Promise((_, reject) => {
814
- setTimeout(() => reject(new Error('bootstrapSecretStorage timed out — the old SSSS key could not be retrieved')), 30000);
815
- });
816
- await Promise.race([bootstrapPromise, timeoutPromise]);
817
827
  }
818
828
  finally {
819
829
  // Always clear transient crypto state so it doesn't bleed into subsequent calls.
@@ -948,7 +958,7 @@ export class MatrixWeb extends WebPlugin {
948
958
  localStorage.setItem(SESSION_KEY, JSON.stringify(session));
949
959
  }
950
960
  serializeEvent(event, fallbackRoomId) {
951
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
961
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
952
962
  const roomId = (_b = (_a = event.getRoomId()) !== null && _a !== void 0 ? _a : fallbackRoomId) !== null && _b !== void 0 ? _b : '';
953
963
  // Redacted events should be marked clearly
954
964
  if (event.isRedacted()) {
@@ -980,7 +990,7 @@ export class MatrixWeb extends WebPlugin {
980
990
  }
981
991
  }
982
992
  }
983
- catch (_k) {
993
+ catch (_l) {
984
994
  // relations may not be available
985
995
  }
986
996
  }
@@ -1010,7 +1020,7 @@ export class MatrixWeb extends WebPlugin {
1010
1020
  }
1011
1021
  }
1012
1022
  }
1013
- catch (_l) {
1023
+ catch (_m) {
1014
1024
  // ignore errors
1015
1025
  }
1016
1026
  }
@@ -1029,7 +1039,7 @@ export class MatrixWeb extends WebPlugin {
1029
1039
  }
1030
1040
  }
1031
1041
  }
1032
- catch (_m) {
1042
+ catch (_o) {
1033
1043
  // ignore
1034
1044
  }
1035
1045
  }
@@ -1038,17 +1048,9 @@ export class MatrixWeb extends WebPlugin {
1038
1048
  const unsigned = unsignedData && Object.keys(unsignedData).length > 0
1039
1049
  ? unsignedData
1040
1050
  : undefined;
1041
- return {
1042
- eventId: eventId !== null && eventId !== void 0 ? eventId : '',
1043
- roomId,
1044
- senderId: sender !== null && sender !== void 0 ? sender : '',
1045
- type: event.getType(),
1046
- content,
1047
- originServerTs: event.getTs(),
1048
- status,
1049
- readBy: readBy.length > 0 ? readBy : undefined,
1050
- unsigned,
1051
- };
1051
+ // Include state_key for state events (e.g. target user in m.room.member)
1052
+ const sk = (_k = event.getStateKey) === null || _k === void 0 ? void 0 : _k.call(event);
1053
+ return Object.assign(Object.assign({ eventId: eventId !== null && eventId !== void 0 ? eventId : '', roomId, senderId: sender !== null && sender !== void 0 ? sender : '', type: event.getType() }, (sk !== undefined && { stateKey: sk })), { content, originServerTs: event.getTs(), status, readBy: readBy.length > 0 ? readBy : undefined, unsigned });
1052
1054
  }
1053
1055
  serializeRoom(room) {
1054
1056
  var _a, _b, _c, _d, _e, _f;