@tagea/capacitor-matrix 1.1.1 → 1.2.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.
@@ -28,6 +28,7 @@ class MatrixSDKBridge {
28
28
  // Keep strong references so GC doesn't cancel subscriptions
29
29
  private var timelineListenerHandles: [Any] = []
30
30
  private var syncStateHandle: TaskHandle?
31
+ private var platformInitialized = false
31
32
  private var syncStateObserver: SyncStateObserverProxy?
32
33
  private let subscriptionLock = NSLock()
33
34
  private var receiptSyncTask: Task<Void, Never>?
@@ -174,6 +175,59 @@ class MatrixSDKBridge {
174
175
  return sessionStore.load()?.toDictionary()
175
176
  }
176
177
 
178
+ func updateAccessToken(accessToken: String) async throws {
179
+ guard client != nil else {
180
+ throw MatrixBridgeError.notLoggedIn
181
+ }
182
+
183
+ // Stop sync service and clean up references
184
+ await syncService?.stop()
185
+ syncService = nil
186
+ syncStateHandle = nil
187
+ receiptSyncTask?.cancel()
188
+ receiptSyncTask = nil
189
+ timelineListenerHandles.removeAll()
190
+ roomTimelines.removeAll()
191
+ subscribedRoomIds.removeAll()
192
+
193
+ guard let oldSession = sessionStore.load() else {
194
+ throw MatrixBridgeError.custom("No persisted session to update")
195
+ }
196
+
197
+ // Build a new client pointing to the same data directory (preserves crypto store).
198
+ // The Rust SDK's restoreSession() can only be called once per Client instance.
199
+ let dataDir = Self.dataDirectory()
200
+ let cacheDir = Self.cacheDirectory()
201
+
202
+ let newClient = try await ClientBuilder()
203
+ .homeserverUrl(url: oldSession.homeserverUrl)
204
+ .sessionPaths(dataPath: dataDir, cachePath: cacheDir)
205
+ .slidingSyncVersionBuilder(versionBuilder: .native)
206
+ .autoEnableCrossSigning(autoEnableCrossSigning: true)
207
+ .build()
208
+
209
+ let newSession = Session(
210
+ accessToken: accessToken,
211
+ refreshToken: nil,
212
+ userId: oldSession.userId,
213
+ deviceId: oldSession.deviceId,
214
+ homeserverUrl: oldSession.homeserverUrl,
215
+ oidcData: nil,
216
+ slidingSyncVersion: .native
217
+ )
218
+
219
+ try await newClient.restoreSession(session: newSession)
220
+ client = newClient
221
+
222
+ let updatedInfo = MatrixSessionInfo(
223
+ accessToken: accessToken,
224
+ userId: oldSession.userId,
225
+ deviceId: oldSession.deviceId,
226
+ homeserverUrl: oldSession.homeserverUrl
227
+ )
228
+ sessionStore.save(session: updatedInfo)
229
+ }
230
+
177
231
  // MARK: - Sync
178
232
 
179
233
  func startSync(
@@ -186,16 +240,19 @@ class MatrixSDKBridge {
186
240
  throw MatrixBridgeError.notLoggedIn
187
241
  }
188
242
 
189
- // Enable Rust SDK tracing to diagnose sync errors
190
- let tracingConfig = TracingConfiguration(
191
- logLevel: .warn,
192
- traceLogPacks: [],
193
- extraTargets: ["matrix_sdk", "matrix_sdk_ui"],
194
- writeToStdoutOrSystem: true,
195
- writeToFiles: nil,
196
- sentryDsn: nil
197
- )
198
- try? initPlatform(config: tracingConfig, useLightweightTokioRuntime: false)
243
+ // Enable Rust SDK tracing (once calling initPlatform twice panics)
244
+ if !platformInitialized {
245
+ let tracingConfig = TracingConfiguration(
246
+ logLevel: .warn,
247
+ traceLogPacks: [],
248
+ extraTargets: ["matrix_sdk", "matrix_sdk_ui"],
249
+ writeToStdoutOrSystem: true,
250
+ writeToFiles: nil,
251
+ sentryDsn: nil
252
+ )
253
+ try? initPlatform(config: tracingConfig, useLightweightTokioRuntime: false)
254
+ platformInitialized = true
255
+ }
199
256
 
200
257
  print("[CapMatrix] startSync: building sync service...")
201
258
  let service = try await c.syncService().finish()
@@ -485,6 +542,23 @@ class MatrixSDKBridge {
485
542
  print("[CapMatrix] room \(roomId): FAILED: \(error)")
486
543
  }
487
544
  }
545
+
546
+ // Preload messages for all rooms in the background so paginateBackwards
547
+ // has already run by the time the user opens a room.
548
+ Task {
549
+ let t0 = CFAbsoluteTimeGetCurrent()
550
+ await withTaskGroup(of: Void.self) { group in
551
+ for (_, roomId) in roomsToSubscribe {
552
+ group.addTask { [weak self] in
553
+ guard let self = self, let timeline = self.roomTimelines[roomId] else { return }
554
+ let tRoom = CFAbsoluteTimeGetCurrent()
555
+ _ = try? await timeline.paginateBackwards(numEvents: 30)
556
+ print("[CapMatrix] [PERF] preload \(roomId.prefix(12))… paginateBackwards=\(self.ms(tRoom, CFAbsoluteTimeGetCurrent()))ms")
557
+ }
558
+ }
559
+ }
560
+ print("[CapMatrix] [PERF] preload ALL rooms done in \(ms(t0, CFAbsoluteTimeGetCurrent()))ms")
561
+ }
488
562
  }
489
563
 
490
564
  func stopSync() async throws {
@@ -624,8 +698,12 @@ class MatrixSDKBridge {
624
698
  }
625
699
 
626
700
  func getRoomMessages(roomId: String, limit: Int, from: String?) async throws -> [String: Any] {
701
+ let t0 = CFAbsoluteTimeGetCurrent()
627
702
  let room = try requireRoom(roomId: roomId)
703
+ let t1 = CFAbsoluteTimeGetCurrent()
628
704
  let timeline = try await getOrCreateTimeline(room: room)
705
+ let t2 = CFAbsoluteTimeGetCurrent()
706
+ print("[CapMatrix] [PERF] getRoomMessages(\(roomId.prefix(12))…) requireRoom=\(ms(t0,t1))ms getOrCreateTimeline=\(ms(t1,t2))ms")
629
707
 
630
708
  // Suppress live listener while we paginate to avoid flooding JS with historical events
631
709
  paginatingLock.lock()
@@ -633,15 +711,20 @@ class MatrixSDKBridge {
633
711
  paginatingLock.unlock()
634
712
 
635
713
  let collector = TimelineItemCollector(roomId: roomId)
714
+ let t3 = CFAbsoluteTimeGetCurrent()
636
715
  let handle = await timeline.addListener(listener: collector)
716
+ let t4 = CFAbsoluteTimeGetCurrent()
717
+ print("[CapMatrix] [PERF] addListener=\(ms(t3,t4))ms")
637
718
 
638
719
  var hitStart = false
639
720
  do {
640
721
  // Wait for the initial Reset snapshot before paginating
722
+ let tWait1 = CFAbsoluteTimeGetCurrent()
641
723
  let gotInitial = await collector.waitForUpdate(timeoutNanos: 5_000_000_000)
724
+ let tWait1Done = CFAbsoluteTimeGetCurrent()
642
725
  let countBefore = collector.events.count
643
726
  let isPagination = from != nil
644
- print("[CapMatrix] getRoomMessages: initial snapshot: \(countBefore) items, gotInitial=\(gotInitial), from=\(from ?? "nil")")
727
+ print("[CapMatrix] [PERF] waitForInitial=\(ms(tWait1,tWait1Done))ms gotInitial=\(gotInitial) items=\(countBefore) from=\(from ?? "nil")")
645
728
 
646
729
  // Reset cursor on initial load
647
730
  if !isPagination {
@@ -650,14 +733,18 @@ class MatrixSDKBridge {
650
733
 
651
734
  // Paginate when: first load with too few items, OR explicit pagination request
652
735
  if isPagination || countBefore < limit {
736
+ let tPag = CFAbsoluteTimeGetCurrent()
653
737
  hitStart = try await timeline.paginateBackwards(numEvents: UInt16(limit))
654
- print("[CapMatrix] getRoomMessages: paginated, hitStart=\(hitStart)")
738
+ let tPagDone = CFAbsoluteTimeGetCurrent()
739
+ print("[CapMatrix] [PERF] paginateBackwards=\(ms(tPag,tPagDone))ms hitStart=\(hitStart)")
655
740
 
656
741
  // If there were new events, wait for the diffs to arrive via the listener
657
742
  if !hitStart {
743
+ let tWait2 = CFAbsoluteTimeGetCurrent()
658
744
  _ = await collector.waitForUpdate(timeoutNanos: 5_000_000_000)
745
+ let tWait2Done = CFAbsoluteTimeGetCurrent()
746
+ print("[CapMatrix] [PERF] waitForPagination=\(ms(tWait2,tWait2Done))ms items=\(collector.events.count)")
659
747
  }
660
- print("[CapMatrix] getRoomMessages: after pagination: \(collector.events.count) items (was \(countBefore))")
661
748
  }
662
749
  } catch {
663
750
  handle.cancel()
@@ -672,38 +759,30 @@ class MatrixSDKBridge {
672
759
  paginatingRooms.remove(roomId)
673
760
  paginatingLock.unlock()
674
761
 
762
+ let tSlice = CFAbsoluteTimeGetCurrent()
675
763
  let allEvents = collector.events
676
764
  var events: [[String: Any]]
677
765
 
678
766
  if let cursorId = oldestReturnedEventId[roomId], from != nil {
679
- // Pagination: find the cursor event and return events before it
680
767
  if let cursorIdx = allEvents.firstIndex(where: { ($0["eventId"] as? String) == cursorId }) {
681
768
  let available = Array(allEvents.prefix(cursorIdx))
682
769
  events = Array(available.suffix(limit))
683
770
  } else {
684
- // Cursor not found (shouldn't happen) — fall back to empty
685
771
  print("[CapMatrix] getRoomMessages: cursor eventId \(cursorId) not found in timeline")
686
772
  events = []
687
773
  }
688
774
  } else {
689
- // Initial load: return newest events
690
775
  events = Array(allEvents.suffix(limit))
691
776
  }
692
777
 
693
- // Update cursor to the oldest event we're returning
694
778
  if let oldest = events.first, let eid = oldest["eventId"] as? String {
695
779
  oldestReturnedEventId[roomId] = eid
696
780
  }
697
781
 
698
- // Apply receipt watermark: if any own event has readBy data,
699
- // all earlier own events in the timeline are also read.
700
- // The SDK only attaches receipts to the specific event they target,
701
- // but in Matrix a read receipt implies all prior events are read too.
702
- // Only events BEFORE the watermark are marked — events after it are unread.
782
+ // Apply receipt watermark
703
783
  let myUserId = client.flatMap({ try? $0.userId() })
704
784
  var watermarkReadBy: [String]? = nil
705
785
  var watermarkIndex = -1
706
- // Walk backwards (newest first) to find the newest own event with a receipt
707
786
  for i in stride(from: events.count - 1, through: 0, by: -1) {
708
787
  let evt = events[i]
709
788
  let sender = evt["senderId"] as? String
@@ -715,7 +794,6 @@ class MatrixSDKBridge {
715
794
  }
716
795
  }
717
796
  }
718
- // Apply watermark only to own events BEFORE the watermark (older)
719
797
  if let watermark = watermarkReadBy, watermarkIndex >= 0 {
720
798
  for i in 0..<watermarkIndex {
721
799
  let sender = events[i]["senderId"] as? String
@@ -728,19 +806,22 @@ class MatrixSDKBridge {
728
806
  }
729
807
  }
730
808
  }
809
+ let tSliceDone = CFAbsoluteTimeGetCurrent()
731
810
 
732
- // Return a pagination token so the JS layer knows more messages are available.
733
- // The Rust SDK timeline handles pagination state internally, so we use a
734
- // synthetic token ("more") to signal that further back-pagination is possible.
735
- // Also stop if pagination returned no new events (timeline fully loaded).
736
811
  let nextBatch: String? = (hitStart || events.isEmpty) ? nil : "more"
737
812
 
813
+ print("[CapMatrix] [PERF] getRoomMessages TOTAL=\(ms(t0,tSliceDone))ms slicing+watermark=\(ms(tSlice,tSliceDone))ms returning \(events.count) events")
814
+
738
815
  return [
739
816
  "events": events,
740
817
  "nextBatch": nextBatch as Any
741
818
  ]
742
819
  }
743
820
 
821
+ private func ms(_ start: CFAbsoluteTime, _ end: CFAbsoluteTime) -> Int {
822
+ return Int((end - start) * 1000)
823
+ }
824
+
744
825
  func markRoomAsRead(roomId: String, eventId: String) async throws {
745
826
  let room = try requireRoom(roomId: roomId)
746
827
  let timeline = try await getOrCreateTimeline(room: room)
@@ -1256,6 +1337,123 @@ class MatrixSDKBridge {
1256
1337
  throw MatrixBridgeError.notSupported("importRoomKeys")
1257
1338
  }
1258
1339
 
1340
+ // MARK: - Presence
1341
+
1342
+ func setPresence(presence: String, statusMsg: String?) async throws {
1343
+ guard let session = sessionStore.load() else {
1344
+ throw MatrixBridgeError.notLoggedIn
1345
+ }
1346
+ let baseUrl = session.homeserverUrl.hasSuffix("/")
1347
+ ? String(session.homeserverUrl.dropLast())
1348
+ : session.homeserverUrl
1349
+ let encodedUserId = session.userId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? session.userId
1350
+ let urlString = "\(baseUrl)/_matrix/client/v3/presence/\(encodedUserId)/status"
1351
+ guard let url = URL(string: urlString) else {
1352
+ throw MatrixBridgeError.notSupported("Invalid presence URL")
1353
+ }
1354
+
1355
+ var body: [String: Any] = ["presence": presence]
1356
+ if let msg = statusMsg { body["status_msg"] = msg }
1357
+ let bodyData = try JSONSerialization.data(withJSONObject: body)
1358
+
1359
+ var request = URLRequest(url: url)
1360
+ request.httpMethod = "PUT"
1361
+ request.setValue("Bearer \(session.accessToken)", forHTTPHeaderField: "Authorization")
1362
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
1363
+ request.httpBody = bodyData
1364
+
1365
+ let (_, response) = try await URLSession.shared.data(for: request)
1366
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
1367
+ guard statusCode >= 200 && statusCode < 300 else {
1368
+ throw MatrixBridgeError.notSupported("setPresence failed with status \(statusCode)")
1369
+ }
1370
+ }
1371
+
1372
+ func getPresence(userId: String) async throws -> [String: Any] {
1373
+ guard let session = sessionStore.load() else {
1374
+ throw MatrixBridgeError.notLoggedIn
1375
+ }
1376
+ let baseUrl = session.homeserverUrl.hasSuffix("/")
1377
+ ? String(session.homeserverUrl.dropLast())
1378
+ : session.homeserverUrl
1379
+ let encodedUserId = userId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? userId
1380
+ let urlString = "\(baseUrl)/_matrix/client/v3/presence/\(encodedUserId)/status"
1381
+ guard let url = URL(string: urlString) else {
1382
+ throw MatrixBridgeError.notSupported("Invalid presence URL")
1383
+ }
1384
+
1385
+ var request = URLRequest(url: url)
1386
+ request.httpMethod = "GET"
1387
+ request.setValue("Bearer \(session.accessToken)", forHTTPHeaderField: "Authorization")
1388
+
1389
+ let (data, response) = try await URLSession.shared.data(for: request)
1390
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
1391
+ guard statusCode >= 200 && statusCode < 300 else {
1392
+ throw MatrixBridgeError.notSupported("getPresence failed with status \(statusCode)")
1393
+ }
1394
+
1395
+ guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
1396
+ throw MatrixBridgeError.notSupported("Invalid presence response")
1397
+ }
1398
+
1399
+ var result: [String: Any] = ["presence": json["presence"] as? String ?? "offline"]
1400
+ if let msg = json["status_msg"] as? String { result["statusMsg"] = msg }
1401
+ if let ago = json["last_active_ago"] as? Int { result["lastActiveAgo"] = ago }
1402
+ return result
1403
+ }
1404
+
1405
+ // MARK: - Pushers
1406
+
1407
+ func setPusher(
1408
+ pushkey: String,
1409
+ kind: String?,
1410
+ appId: String,
1411
+ appDisplayName: String,
1412
+ deviceDisplayName: String,
1413
+ lang: String,
1414
+ dataUrl: String,
1415
+ dataFormat: String?
1416
+ ) async throws {
1417
+ guard let session = sessionStore.load() else {
1418
+ throw MatrixBridgeError.notLoggedIn
1419
+ }
1420
+ let baseUrl = session.homeserverUrl.hasSuffix("/")
1421
+ ? String(session.homeserverUrl.dropLast())
1422
+ : session.homeserverUrl
1423
+ let urlString = "\(baseUrl)/_matrix/client/v3/pushers/set"
1424
+ guard let url = URL(string: urlString) else {
1425
+ throw MatrixBridgeError.notSupported("Invalid pushers URL")
1426
+ }
1427
+
1428
+ var dataObj: [String: Any] = ["url": dataUrl]
1429
+ if let format = dataFormat { dataObj["format"] = format }
1430
+
1431
+ var body: [String: Any] = [
1432
+ "pushkey": pushkey,
1433
+ "kind": kind as Any,
1434
+ "app_id": appId,
1435
+ "app_display_name": appDisplayName,
1436
+ "device_display_name": deviceDisplayName,
1437
+ "lang": lang,
1438
+ "data": dataObj,
1439
+ ]
1440
+ if kind == nil { body["kind"] = NSNull() }
1441
+
1442
+ let bodyData = try JSONSerialization.data(withJSONObject: body)
1443
+
1444
+ var request = URLRequest(url: url)
1445
+ request.httpMethod = "POST"
1446
+ request.setValue("Bearer \(session.accessToken)", forHTTPHeaderField: "Authorization")
1447
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
1448
+ request.httpBody = bodyData
1449
+
1450
+ let (_, response) = try await URLSession.shared.data(for: request)
1451
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
1452
+ guard statusCode >= 200 && statusCode < 300 else {
1453
+ throw MatrixBridgeError.notSupported("setPusher failed with status \(statusCode)")
1454
+ }
1455
+ }
1456
+
1259
1457
  // MARK: - Helpers
1260
1458
 
1261
1459
  private static func dataDirectory() -> String {
@@ -1291,18 +1489,25 @@ class MatrixSDKBridge {
1291
1489
  let isDirect = info.isDirect
1292
1490
  let avatarUrl: String? = nil // Rust SDK doesn't expose avatar URL via RoomInfo yet
1293
1491
 
1294
- return [
1492
+ let latestEvent = await room.latestEvent()
1493
+ let latestEventDict = serializeLatestEventValue(latestEvent, roomId: room.id())
1494
+
1495
+ var dict: [String: Any] = [
1295
1496
  "roomId": room.id(),
1296
1497
  "name": info.displayName ?? "",
1297
1498
  "topic": info.topic as Any,
1298
1499
  "memberCount": info.joinedMembersCount ?? 0,
1299
1500
  "isEncrypted": encrypted,
1300
1501
  "unreadCount": info.numUnreadMessages ?? 0,
1301
- "lastEventTs": nil as Int? as Any,
1502
+ "lastEventTs": latestEventDict?["originServerTs"] as Any,
1302
1503
  "membership": membership,
1303
1504
  "avatarUrl": avatarUrl as Any,
1304
1505
  "isDirect": isDirect,
1305
1506
  ]
1507
+ if let le = latestEventDict {
1508
+ dict["latestEvent"] = le
1509
+ }
1510
+ return dict
1306
1511
  }
1307
1512
 
1308
1513
  private static func mapSyncState(_ state: SyncServiceState) -> String {
@@ -1323,6 +1528,89 @@ class MatrixSDKBridge {
1323
1528
 
1324
1529
  // MARK: - Timeline Serialization Helpers
1325
1530
 
1531
+ /// Serialize a LatestEventValue (from room.latestEvent()) into a lightweight dictionary
1532
+ /// for last-message previews. Does NOT create a timeline subscription.
1533
+ private func serializeLatestEventValue(_ value: LatestEventValue, roomId: String) -> [String: Any]? {
1534
+ let timestamp: UInt64
1535
+ let sender: String
1536
+ let profile: ProfileDetails
1537
+ let content: TimelineItemContent
1538
+
1539
+ switch value {
1540
+ case .none:
1541
+ return nil
1542
+ case .remote(let ts, let s, _, let p, let c):
1543
+ timestamp = ts
1544
+ sender = s
1545
+ profile = p
1546
+ content = c
1547
+ case .local(let ts, let s, let p, let c, _):
1548
+ timestamp = ts
1549
+ sender = s
1550
+ profile = p
1551
+ content = c
1552
+ @unknown default:
1553
+ return nil
1554
+ }
1555
+
1556
+ var contentDict: [String: Any] = [:]
1557
+ var eventType = "m.room.message"
1558
+
1559
+ switch content {
1560
+ case .msgLike(let msgLikeContent):
1561
+ switch msgLikeContent.kind {
1562
+ case .message(let messageContent):
1563
+ contentDict["body"] = messageContent.body
1564
+ switch messageContent.msgType {
1565
+ case .text:
1566
+ contentDict["msgtype"] = "m.text"
1567
+ case .image:
1568
+ contentDict["msgtype"] = "m.image"
1569
+ case .file:
1570
+ contentDict["msgtype"] = "m.file"
1571
+ case .audio:
1572
+ contentDict["msgtype"] = "m.audio"
1573
+ case .video:
1574
+ contentDict["msgtype"] = "m.video"
1575
+ case .emote:
1576
+ contentDict["msgtype"] = "m.emote"
1577
+ case .notice:
1578
+ contentDict["msgtype"] = "m.notice"
1579
+ default:
1580
+ contentDict["msgtype"] = "m.text"
1581
+ }
1582
+ case .unableToDecrypt:
1583
+ contentDict["body"] = "Unable to decrypt message"
1584
+ contentDict["msgtype"] = "m.text"
1585
+ contentDict["encrypted"] = true
1586
+ case .redacted:
1587
+ eventType = "m.room.redaction"
1588
+ contentDict["body"] = "Message deleted"
1589
+ default:
1590
+ eventType = "m.room.unknown"
1591
+ }
1592
+ default:
1593
+ eventType = "m.room.unknown"
1594
+ }
1595
+
1596
+ var senderDisplayName: String? = nil
1597
+ if case .ready(let displayName, _, _) = profile {
1598
+ senderDisplayName = displayName
1599
+ }
1600
+
1601
+ var dict: [String: Any] = [
1602
+ "roomId": roomId,
1603
+ "senderId": sender,
1604
+ "type": eventType,
1605
+ "content": contentDict,
1606
+ "originServerTs": timestamp,
1607
+ ]
1608
+ if let name = senderDisplayName {
1609
+ dict["senderDisplayName"] = name
1610
+ }
1611
+ return dict
1612
+ }
1613
+
1326
1614
  /// Extract the event ID string from an EventOrTransactionId enum.
1327
1615
  private func extractEventId(_ eventOrTxnId: EventOrTransactionId) -> String? {
1328
1616
  switch eventOrTxnId {
@@ -1417,6 +1705,35 @@ private func serializeEventTimelineItem(_ eventItem: EventTimelineItem, roomId:
1417
1705
  ] as [String: Any]
1418
1706
  }
1419
1707
  }
1708
+ case .roomMembership(let userId, let userDisplayName, let change, _):
1709
+ eventType = "m.room.member"
1710
+ let membership: String
1711
+ switch change {
1712
+ case .joined, .invitationAccepted:
1713
+ membership = "join"
1714
+ case .left:
1715
+ membership = "leave"
1716
+ case .banned, .kickedAndBanned:
1717
+ membership = "ban"
1718
+ case .invited:
1719
+ membership = "invite"
1720
+ case .kicked:
1721
+ membership = "leave"
1722
+ case .unbanned:
1723
+ membership = "leave"
1724
+ default:
1725
+ membership = "join"
1726
+ }
1727
+ contentDict["membership"] = membership
1728
+ contentDict["displayname"] = userDisplayName ?? userId
1729
+ contentDict["stateKey"] = userId
1730
+ case .state(_, let stateContent):
1731
+ switch stateContent {
1732
+ case .roomCreate:
1733
+ eventType = "m.room.create"
1734
+ default:
1735
+ eventType = "m.room.unknown"
1736
+ }
1420
1737
  default:
1421
1738
  eventType = "m.room.unknown"
1422
1739
  }
@@ -1479,7 +1796,7 @@ class LiveTimelineListener: TimelineListener {
1479
1796
  self.isPaginating = isPaginating
1480
1797
  }
1481
1798
 
1482
- /// Emit a room update with the current unread count fetched from the room.
1799
+ /// Emit a room update with unread count and latest event preview.
1483
1800
  private func emitRoomUpdate() {
1484
1801
  Task {
1485
1802
  let unreadCount: Int
@@ -1488,7 +1805,12 @@ class LiveTimelineListener: TimelineListener {
1488
1805
  } else {
1489
1806
  unreadCount = 0
1490
1807
  }
1491
- onRoomUpdate(roomId, ["roomId": roomId, "unreadCount": unreadCount])
1808
+ var summary: [String: Any] = ["roomId": roomId, "unreadCount": unreadCount]
1809
+ let latestEvent = await room.latestEvent()
1810
+ if let le = serializeLatestEventValue(latestEvent, roomId: roomId) {
1811
+ summary["latestEvent"] = le
1812
+ }
1813
+ onRoomUpdate(roomId, summary)
1492
1814
  }
1493
1815
  }
1494
1816
 
@@ -58,6 +58,7 @@ public class MatrixPlugin: CAPPlugin, CAPBridgedPlugin {
58
58
  CAPPluginMethod(name: "setPusher", returnType: CAPPluginReturnPromise),
59
59
  CAPPluginMethod(name: "verifyDevice", returnType: CAPPluginReturnPromise),
60
60
  CAPPluginMethod(name: "clearAllData", returnType: CAPPluginReturnPromise),
61
+ CAPPluginMethod(name: "updateAccessToken", returnType: CAPPluginReturnPromise),
61
62
  ]
62
63
 
63
64
  private let matrixBridge = MatrixSDKBridge()
@@ -118,6 +119,21 @@ public class MatrixPlugin: CAPPlugin, CAPBridgedPlugin {
118
119
  call.resolve()
119
120
  }
120
121
 
122
+ @objc func updateAccessToken(_ call: CAPPluginCall) {
123
+ guard let accessToken = call.getString("accessToken") else {
124
+ return call.reject("Missing accessToken")
125
+ }
126
+
127
+ Task {
128
+ do {
129
+ try await matrixBridge.updateAccessToken(accessToken: accessToken)
130
+ call.resolve()
131
+ } catch {
132
+ call.reject(error.localizedDescription)
133
+ }
134
+ }
135
+ }
136
+
121
137
  @objc func getSession(_ call: CAPPluginCall) {
122
138
  if let session = matrixBridge.getSession() {
123
139
  call.resolve(session)
@@ -661,11 +677,34 @@ public class MatrixPlugin: CAPPlugin, CAPBridgedPlugin {
661
677
  }
662
678
 
663
679
  @objc func setPresence(_ call: CAPPluginCall) {
664
- call.reject("setPresence is not supported on this platform")
680
+ guard let presence = call.getString("presence") else {
681
+ return call.reject("Missing presence")
682
+ }
683
+ let statusMsg = call.getString("statusMsg")
684
+
685
+ Task {
686
+ do {
687
+ try await matrixBridge.setPresence(presence: presence, statusMsg: statusMsg)
688
+ call.resolve()
689
+ } catch {
690
+ call.reject(error.localizedDescription)
691
+ }
692
+ }
665
693
  }
666
694
 
667
695
  @objc func getPresence(_ call: CAPPluginCall) {
668
- call.reject("getPresence is not supported on this platform")
696
+ guard let userId = call.getString("userId") else {
697
+ return call.reject("Missing userId")
698
+ }
699
+
700
+ Task {
701
+ do {
702
+ let result = try await matrixBridge.getPresence(userId: userId)
703
+ call.resolve(result)
704
+ } catch {
705
+ call.reject(error.localizedDescription)
706
+ }
707
+ }
669
708
  }
670
709
 
671
710
  @objc func forgetRoom(_ call: CAPPluginCall) {
@@ -811,6 +850,34 @@ public class MatrixPlugin: CAPPlugin, CAPBridgedPlugin {
811
850
  }
812
851
 
813
852
  @objc func setPusher(_ call: CAPPluginCall) {
814
- call.reject("setPusher is not yet supported")
853
+ guard let pushkey = call.getString("pushkey"),
854
+ let appId = call.getString("appId"),
855
+ let appDisplayName = call.getString("appDisplayName"),
856
+ let deviceDisplayName = call.getString("deviceDisplayName"),
857
+ let lang = call.getString("lang"),
858
+ let dataObj = call.getObject("data"),
859
+ let dataUrl = dataObj["url"] as? String else {
860
+ return call.reject("Missing required parameters")
861
+ }
862
+ let kind = call.getString("kind")
863
+ let dataFormat = dataObj["format"] as? String
864
+
865
+ Task {
866
+ do {
867
+ try await matrixBridge.setPusher(
868
+ pushkey: pushkey,
869
+ kind: kind,
870
+ appId: appId,
871
+ appDisplayName: appDisplayName,
872
+ deviceDisplayName: deviceDisplayName,
873
+ lang: lang,
874
+ dataUrl: dataUrl,
875
+ dataFormat: dataFormat
876
+ )
877
+ call.resolve()
878
+ } catch {
879
+ call.reject(error.localizedDescription)
880
+ }
881
+ }
815
882
  }
816
883
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagea/capacitor-matrix",
3
- "version": "1.1.1",
3
+ "version": "1.2.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",