@tagea/capacitor-matrix 0.3.2 → 1.0.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/Package.swift CHANGED
@@ -11,7 +11,7 @@ let package = Package(
11
11
  ],
12
12
  dependencies: [
13
13
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
14
- .package(url: "https://github.com/matrix-org/matrix-rust-components-swift.git", exact: "1.2.0")
14
+ .package(url: "https://github.com/matrix-org/matrix-rust-components-swift.git", exact: "26.01.04")
15
15
  ],
16
16
  targets: [
17
17
  .target(
@@ -62,7 +62,7 @@ dependencies {
62
62
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
63
63
  implementation "androidx.core:core-ktx:$androidxCoreKTXVersion"
64
64
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
65
- implementation "org.matrix.rustcomponents:sdk-android:26.03.11"
65
+ implementation "org.matrix.rustcomponents:sdk-android:26.03.23"
66
66
  implementation "androidx.security:security-crypto:1.1.0-alpha06"
67
67
  testImplementation "junit:junit:$junitVersion"
68
68
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
@@ -127,7 +127,7 @@ class MatrixSDKBridge {
127
127
  func logout() async throws {
128
128
  receiptSyncTask?.cancel()
129
129
  receiptSyncTask = nil
130
- try await syncService?.stop()
130
+ await syncService?.stop()
131
131
  syncService = nil
132
132
  syncStateHandle = nil
133
133
  timelineListenerHandles.removeAll()
@@ -172,11 +172,14 @@ class MatrixSDKBridge {
172
172
 
173
173
  // Enable Rust SDK tracing to diagnose sync errors
174
174
  let tracingConfig = TracingConfiguration(
175
- filter: "warn,matrix_sdk=debug,matrix_sdk_ui=debug",
175
+ logLevel: .warn,
176
+ traceLogPacks: [],
177
+ extraTargets: ["matrix_sdk", "matrix_sdk_ui"],
176
178
  writeToStdoutOrSystem: true,
177
- writeToFiles: nil
179
+ writeToFiles: nil,
180
+ sentryDsn: nil
178
181
  )
179
- setupTracing(config: tracingConfig)
182
+ try? initPlatform(config: tracingConfig, useLightweightTokioRuntime: false)
180
183
 
181
184
  print("[CapMatrix] startSync: building sync service...")
182
185
  let service = try await c.syncService().finish()
@@ -456,7 +459,7 @@ class MatrixSDKBridge {
456
459
  subscriptionLock.lock()
457
460
  timelineListenerHandles.append(handle)
458
461
  subscriptionLock.unlock()
459
- print("[CapMatrix] room \(roomId): listener added")
462
+ print("[CapMatrix] room \(roomId): listener added")
460
463
  } catch {
461
464
  print("[CapMatrix] room \(roomId): FAILED: \(error)")
462
465
  }
@@ -464,7 +467,7 @@ class MatrixSDKBridge {
464
467
  }
465
468
 
466
469
  func stopSync() async throws {
467
- try await syncService?.stop()
470
+ await syncService?.stop()
468
471
  syncStateHandle = nil
469
472
  subscribedRoomIds.removeAll()
470
473
  timelineListenerHandles.removeAll()
@@ -585,7 +588,9 @@ class MatrixSDKBridge {
585
588
  func editMessage(roomId: String, eventId: String, newBody: String) async throws -> String {
586
589
  let room = try requireRoom(roomId: roomId)
587
590
  let content = messageEventContentFromMarkdown(md: newBody)
588
- try await room.edit(eventId: eventId, newContent: content)
591
+ let editContent = EditedContent.roomMessage(content: content)
592
+ let timeline = try await getOrCreateTimeline(room: room)
593
+ try await timeline.edit(eventOrTransactionId: .eventId(eventId: eventId), newContent: editContent)
589
594
  return ""
590
595
  }
591
596
 
@@ -730,24 +735,14 @@ class MatrixSDKBridge {
730
735
 
731
736
  func redactEvent(roomId: String, eventId: String, reason: String?) async throws {
732
737
  let room = try requireRoom(roomId: roomId)
733
- try await room.redact(eventId: eventId, reason: reason)
738
+ let timeline = try await getOrCreateTimeline(room: room)
739
+ try await timeline.redactEvent(eventOrTransactionId: .eventId(eventId: eventId), reason: reason)
734
740
  }
735
741
 
736
742
  func sendReaction(roomId: String, eventId: String, key: String) async throws {
737
743
  let room = try requireRoom(roomId: roomId)
738
744
  let timeline = try await getOrCreateTimeline(room: room)
739
-
740
- // toggleReaction needs the timeline item's uniqueId, not the eventId
741
- // addListener immediately fires a Reset diff with current items
742
- let collector = TimelineItemCollector(roomId: roomId)
743
- let handle = await timeline.addListener(listener: collector)
744
- await collector.waitForUpdate()
745
- handle.cancel()
746
-
747
- guard let uniqueId = collector.uniqueIdForEvent(eventId) else {
748
- throw MatrixBridgeError.notSupported("Could not find timeline item for event \(eventId)")
749
- }
750
- try await timeline.toggleReaction(uniqueId: uniqueId, key: key)
745
+ _ = try await timeline.toggleReaction(itemId: .eventId(eventId: eventId), key: key)
751
746
  }
752
747
 
753
748
  // MARK: - User Discovery
@@ -1002,6 +997,7 @@ class MatrixSDKBridge {
1002
997
  let listener = NoopEnableRecoveryProgressListener()
1003
998
  let key = try await c.encryption().enableRecovery(
1004
999
  waitForBackupsToUpload: false,
1000
+ passphrase: passphrase,
1005
1001
  progressListener: listener
1006
1002
  )
1007
1003
  return ["recoveryKey": key]
@@ -1054,7 +1050,7 @@ class MatrixSDKBridge {
1054
1050
 
1055
1051
  private static func serializeRoom(_ room: Room) async throws -> [String: Any] {
1056
1052
  let info = try await room.roomInfo()
1057
- let encrypted = (try? room.isEncrypted()) ?? false
1053
+ let encrypted = await room.isEncrypted()
1058
1054
  let membership: String = {
1059
1055
  switch room.membership() {
1060
1056
  case .joined: return "join"
@@ -1098,6 +1094,18 @@ class MatrixSDKBridge {
1098
1094
 
1099
1095
  // MARK: - Timeline Serialization Helpers
1100
1096
 
1097
+ /// Extract the event ID string from an EventOrTransactionId enum.
1098
+ private func extractEventId(_ eventOrTxnId: EventOrTransactionId) -> String? {
1099
+ switch eventOrTxnId {
1100
+ case .eventId(let eventId):
1101
+ return eventId
1102
+ case .transactionId(let transactionId):
1103
+ return transactionId
1104
+ @unknown default:
1105
+ return nil
1106
+ }
1107
+ }
1108
+
1101
1109
  private func extractMediaUrl(source: MediaSource, into contentDict: inout [String: Any]) {
1102
1110
  let url = source.url()
1103
1111
  if !url.isEmpty {
@@ -1120,24 +1128,20 @@ private func serializeTimelineItem(_ item: TimelineItem, roomId: String) -> [Str
1120
1128
  }
1121
1129
 
1122
1130
  private func serializeEventTimelineItem(_ eventItem: EventTimelineItem, roomId: String) -> [String: Any]? {
1123
- let eventId: String
1124
- if let eid = eventItem.eventId() {
1125
- eventId = eid
1126
- } else if let tid = eventItem.transactionId() {
1127
- eventId = tid
1128
- } else {
1131
+ guard let eventId = extractEventId(eventItem.eventOrTransactionId) else {
1129
1132
  return nil
1130
1133
  }
1131
1134
 
1132
1135
  var contentDict: [String: Any] = [:]
1133
1136
  var eventType = "m.room.message"
1134
1137
 
1135
- let content = eventItem.content()
1136
- switch content.kind() {
1137
- case .message:
1138
- if let msg = content.asMessage() {
1139
- contentDict["body"] = msg.body()
1140
- switch msg.msgtype() {
1138
+ let content = eventItem.content
1139
+ switch content {
1140
+ case .msgLike(let msgLikeContent):
1141
+ switch msgLikeContent.kind {
1142
+ case .message(let messageContent):
1143
+ contentDict["body"] = messageContent.body
1144
+ switch messageContent.msgType {
1141
1145
  case .text:
1142
1146
  contentDict["msgtype"] = "m.text"
1143
1147
  case .image(let imgContent):
@@ -1162,39 +1166,41 @@ private func serializeEventTimelineItem(_ eventItem: EventTimelineItem, roomId:
1162
1166
  default:
1163
1167
  contentDict["msgtype"] = "m.text"
1164
1168
  }
1169
+ case .unableToDecrypt:
1170
+ contentDict["body"] = "Unable to decrypt message"
1171
+ contentDict["msgtype"] = "m.text"
1172
+ contentDict["encrypted"] = true
1173
+ case .redacted:
1174
+ eventType = "m.room.redaction"
1175
+ contentDict["body"] = "Message deleted"
1176
+ default:
1177
+ eventType = "m.room.unknown"
1165
1178
  }
1166
- case .unableToDecrypt:
1167
- contentDict["body"] = "Unable to decrypt message"
1168
- contentDict["msgtype"] = "m.text"
1169
- contentDict["encrypted"] = true
1170
- case .redactedMessage:
1171
- eventType = "m.room.redaction"
1172
- contentDict["body"] = "Message deleted"
1173
- default:
1174
- eventType = "m.room.unknown"
1175
- }
1176
1179
 
1177
- // Reactions
1178
- let reactions = eventItem.reactions()
1179
- if !reactions.isEmpty {
1180
- contentDict["reactions"] = reactions.map { r in
1181
- [
1182
- "key": r.key,
1183
- "count": r.senders.count,
1184
- "senders": r.senders.map { $0.senderId },
1185
- ] as [String: Any]
1180
+ // Reactions from MsgLikeContent
1181
+ let reactions = msgLikeContent.reactions
1182
+ if !reactions.isEmpty {
1183
+ contentDict["reactions"] = reactions.map { r in
1184
+ [
1185
+ "key": r.key,
1186
+ "count": r.senders.count,
1187
+ "senders": r.senders.map { $0.senderId },
1188
+ ] as [String: Any]
1189
+ }
1186
1190
  }
1191
+ default:
1192
+ eventType = "m.room.unknown"
1187
1193
  }
1188
1194
 
1189
1195
  // Delivery/read status
1190
1196
  var status: String = "sent"
1191
- if let sendState = eventItem.localSendState() {
1197
+ if let sendState = eventItem.localSendState {
1192
1198
  switch sendState {
1193
1199
  case .notSentYet:
1194
1200
  status = "sending"
1195
- case .sendingFailed(_, _):
1201
+ case .sendingFailed:
1196
1202
  status = "sending"
1197
- case .sent(_):
1203
+ case .sent:
1198
1204
  // Check read receipts below
1199
1205
  break
1200
1206
  default:
@@ -1203,12 +1209,12 @@ private func serializeEventTimelineItem(_ eventItem: EventTimelineItem, roomId:
1203
1209
  }
1204
1210
 
1205
1211
  var readBy: [String]? = nil
1206
- let receipts = eventItem.readReceipts()
1212
+ let receipts = eventItem.readReceipts
1207
1213
  if !receipts.isEmpty {
1208
- print("[CapMatrix] readReceipts for \(eventId): \(receipts.keys) sender=\(eventItem.sender())")
1214
+ print("[CapMatrix] readReceipts for \(eventId): \(receipts.keys) sender=\(eventItem.sender)")
1209
1215
  }
1210
1216
  if status == "sent" {
1211
- let others = receipts.keys.filter { $0 != eventItem.sender() }
1217
+ let others = receipts.keys.filter { $0 != eventItem.sender }
1212
1218
  if !others.isEmpty {
1213
1219
  status = "read"
1214
1220
  readBy = Array(others)
@@ -1218,10 +1224,10 @@ private func serializeEventTimelineItem(_ eventItem: EventTimelineItem, roomId:
1218
1224
  return [
1219
1225
  "eventId": eventId,
1220
1226
  "roomId": roomId,
1221
- "senderId": eventItem.sender(),
1227
+ "senderId": eventItem.sender,
1222
1228
  "type": eventType,
1223
1229
  "content": contentDict,
1224
- "originServerTs": eventItem.timestamp(),
1230
+ "originServerTs": eventItem.timestamp,
1225
1231
  "status": status,
1226
1232
  "readBy": readBy as Any,
1227
1233
  ]
@@ -1243,11 +1249,8 @@ class LiveTimelineListener: TimelineListener {
1243
1249
  func onUpdate(diff: [TimelineDiff]) {
1244
1250
  print("[CapMatrix] LiveTimelineListener onUpdate for \(roomId): \(diff.count) diffs")
1245
1251
  for d in diff {
1246
- let change = d.change()
1247
- print("[CapMatrix] diff type: \(change)")
1248
- switch change {
1249
- case .reset:
1250
- let items = d.reset() ?? []
1252
+ switch d {
1253
+ case .reset(let items):
1251
1254
  print("[CapMatrix] Reset: \(items.count) items")
1252
1255
  items.forEach { item in
1253
1256
  if let event = serializeTimelineItem(item, roomId: roomId) {
@@ -1256,8 +1259,7 @@ class LiveTimelineListener: TimelineListener {
1256
1259
  }
1257
1260
  }
1258
1261
  onRoomUpdate(roomId, ["roomId": roomId])
1259
- case .append:
1260
- let items = d.append() ?? []
1262
+ case .append(let items):
1261
1263
  print("[CapMatrix] Append: \(items.count) items")
1262
1264
  items.forEach { item in
1263
1265
  if let event = serializeTimelineItem(item, roomId: roomId) {
@@ -1266,44 +1268,45 @@ class LiveTimelineListener: TimelineListener {
1266
1268
  }
1267
1269
  }
1268
1270
  onRoomUpdate(roomId, ["roomId": roomId])
1269
- case .pushBack:
1270
- if let item = d.pushBack() {
1271
- let isLocalEcho = item.asEvent()?.eventId() == nil && item.asEvent()?.transactionId() != nil
1272
- print("[CapMatrix] PushBack: localEcho=\(isLocalEcho)")
1273
- if !isLocalEcho, let event = serializeTimelineItem(item, roomId: roomId) {
1274
- print("[CapMatrix] PushBack item: \(event["eventId"] ?? "nil") type=\(event["type"] ?? "nil")")
1275
- onMessage(event)
1271
+ case .pushBack(let item):
1272
+ let isLocalEcho = item.asEvent().map { extractEventId($0.eventOrTransactionId) } != nil
1273
+ && item.asEvent()?.eventOrTransactionId is EventOrTransactionId
1274
+ // Check if this is a local echo (transaction ID, no event ID yet)
1275
+ let eventItem = item.asEvent()
1276
+ var skipLocalEcho = false
1277
+ if let ei = eventItem {
1278
+ if case .transactionId = ei.eventOrTransactionId {
1279
+ skipLocalEcho = true
1276
1280
  }
1277
- onRoomUpdate(roomId, ["roomId": roomId])
1278
1281
  }
1279
- case .pushFront:
1280
- if let item = d.pushFront() {
1281
- if let event = serializeTimelineItem(item, roomId: roomId) {
1282
- print("[CapMatrix] PushFront item: \(event["eventId"] ?? "nil")")
1283
- onMessage(event)
1284
- }
1285
- onRoomUpdate(roomId, ["roomId": roomId])
1282
+ print("[CapMatrix] PushBack: localEcho=\(skipLocalEcho)")
1283
+ if !skipLocalEcho, let event = serializeTimelineItem(item, roomId: roomId) {
1284
+ print("[CapMatrix] PushBack item: \(event["eventId"] ?? "nil") type=\(event["type"] ?? "nil")")
1285
+ onMessage(event)
1286
1286
  }
1287
- case .set:
1288
- if let data = d.set() {
1289
- if let event = serializeTimelineItem(data.item, roomId: roomId) {
1290
- print("[CapMatrix] Set item: \(event["eventId"] ?? "nil") type=\(event["type"] ?? "nil") status=\(event["status"] ?? "nil") readBy=\(event["readBy"] ?? "nil")")
1291
- onMessage(event)
1292
- // If this event has readBy data, trigger roomUpdated
1293
- // so the app can refresh receipt status for all messages
1294
- if let rb = event["readBy"] as? [String], !rb.isEmpty {
1295
- onRoomUpdate(roomId, ["roomId": roomId])
1296
- }
1297
- }
1287
+ onRoomUpdate(roomId, ["roomId": roomId])
1288
+ case .pushFront(let item):
1289
+ if let event = serializeTimelineItem(item, roomId: roomId) {
1290
+ print("[CapMatrix] PushFront item: \(event["eventId"] ?? "nil")")
1291
+ onMessage(event)
1298
1292
  }
1299
- case .insert:
1300
- if let data = d.insert() {
1301
- if let event = serializeTimelineItem(data.item, roomId: roomId) {
1302
- print("[CapMatrix] Insert item: \(event["eventId"] ?? "nil")")
1303
- onMessage(event)
1293
+ onRoomUpdate(roomId, ["roomId": roomId])
1294
+ case .set(let index, let item):
1295
+ if let event = serializeTimelineItem(item, roomId: roomId) {
1296
+ print("[CapMatrix] Set item: \(event["eventId"] ?? "nil") type=\(event["type"] ?? "nil") status=\(event["status"] ?? "nil") readBy=\(event["readBy"] ?? "nil")")
1297
+ onMessage(event)
1298
+ // If this event has readBy data, trigger roomUpdated
1299
+ // so the app can refresh receipt status for all messages
1300
+ if let rb = event["readBy"] as? [String], !rb.isEmpty {
1301
+ onRoomUpdate(roomId, ["roomId": roomId])
1304
1302
  }
1305
- onRoomUpdate(roomId, ["roomId": roomId])
1306
1303
  }
1304
+ case .insert(let index, let item):
1305
+ if let event = serializeTimelineItem(item, roomId: roomId) {
1306
+ print("[CapMatrix] Insert item: \(event["eventId"] ?? "nil")")
1307
+ onMessage(event)
1308
+ }
1309
+ onRoomUpdate(roomId, ["roomId": roomId])
1307
1310
  case .remove:
1308
1311
  break // Index-based removal, handled by JS layer
1309
1312
  default:
@@ -1401,46 +1404,50 @@ class TimelineItemCollector: TimelineListener {
1401
1404
  var continuation: CheckedContinuation<Bool, Never>?
1402
1405
  lock.lock()
1403
1406
  for d in diff {
1404
- switch d.change() {
1405
- case .reset:
1407
+ switch d {
1408
+ case .reset(let items):
1406
1409
  _items.removeAll()
1407
1410
  _uniqueIdMap.removeAll()
1408
- d.reset()?.forEach { item in
1409
- trackUniqueId(item)
1410
- _items.append(serializeTimelineItem(item, roomId: roomId))
1411
- }
1412
- case .append:
1413
- d.append()?.forEach { item in
1411
+ items.forEach { item in
1414
1412
  trackUniqueId(item)
1415
1413
  _items.append(serializeTimelineItem(item, roomId: roomId))
1416
1414
  }
1417
- case .pushBack:
1418
- if let item = d.pushBack() {
1415
+ case .append(let items):
1416
+ items.forEach { item in
1419
1417
  trackUniqueId(item)
1420
1418
  _items.append(serializeTimelineItem(item, roomId: roomId))
1421
1419
  }
1422
- case .pushFront:
1423
- if let item = d.pushFront() {
1424
- trackUniqueId(item)
1425
- _items.insert(serializeTimelineItem(item, roomId: roomId), at: 0)
1426
- }
1427
- case .set:
1428
- if let data = d.set() {
1429
- trackUniqueId(data.item)
1430
- let idx = Int(data.index)
1431
- if idx >= 0 && idx < _items.count {
1432
- _items[idx] = serializeTimelineItem(data.item, roomId: roomId)
1433
- }
1434
- }
1435
- case .insert:
1436
- if let data = d.insert() {
1437
- trackUniqueId(data.item)
1438
- let idx = min(Int(data.index), _items.count)
1439
- _items.insert(serializeTimelineItem(data.item, roomId: roomId), at: idx)
1420
+ case .pushBack(let item):
1421
+ trackUniqueId(item)
1422
+ _items.append(serializeTimelineItem(item, roomId: roomId))
1423
+ case .pushFront(let item):
1424
+ trackUniqueId(item)
1425
+ _items.insert(serializeTimelineItem(item, roomId: roomId), at: 0)
1426
+ case .set(let index, let item):
1427
+ trackUniqueId(item)
1428
+ let idx = Int(index)
1429
+ if idx >= 0 && idx < _items.count {
1430
+ _items[idx] = serializeTimelineItem(item, roomId: roomId)
1440
1431
  }
1432
+ case .insert(let index, let item):
1433
+ trackUniqueId(item)
1434
+ let idx = min(Int(index), _items.count)
1435
+ _items.insert(serializeTimelineItem(item, roomId: roomId), at: idx)
1441
1436
  case .clear:
1442
1437
  _items.removeAll()
1443
1438
  _uniqueIdMap.removeAll()
1439
+ case .remove(let index):
1440
+ let idx = Int(index)
1441
+ if idx >= 0 && idx < _items.count {
1442
+ _items.remove(at: idx)
1443
+ }
1444
+ case .truncate(let length):
1445
+ let len = Int(length)
1446
+ while _items.count > len { _items.removeLast() }
1447
+ case .popBack:
1448
+ if !_items.isEmpty { _items.removeLast() }
1449
+ case .popFront:
1450
+ if !_items.isEmpty { _items.removeFirst() }
1444
1451
  default:
1445
1452
  break
1446
1453
  }
@@ -1454,13 +1461,10 @@ class TimelineItemCollector: TimelineListener {
1454
1461
 
1455
1462
  private func trackUniqueId(_ item: TimelineItem) {
1456
1463
  guard let eventItem = item.asEvent() else { return }
1457
- let uniqueId = item.uniqueId()
1458
- if let eid = eventItem.eventId() {
1464
+ let uniqueId = item.uniqueId().id
1465
+ if let eid = extractEventId(eventItem.eventOrTransactionId) {
1459
1466
  _uniqueIdMap[eid] = uniqueId
1460
1467
  }
1461
- if let tid = eventItem.transactionId() {
1462
- _uniqueIdMap[tid] = uniqueId
1463
- }
1464
1468
  }
1465
1469
  }
1466
1470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagea/capacitor-matrix",
3
- "version": "0.3.2",
3
+ "version": "1.0.0",
4
4
  "description": "A capacitor plugin wrapping native matrix SDKs",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",