@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.
- package/README.md +31 -0
- package/android/src/main/kotlin/de/tremaze/capacitor/matrix/CapMatrix.kt +142 -0
- package/android/src/main/kotlin/de/tremaze/capacitor/matrix/CapMatrixPlugin.kt +53 -3
- package/dist/docs.json +39 -0
- package/dist/esm/definitions.d.ts +4 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +39 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +39 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +39 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapMatrixPlugin/CapMatrix.swift +354 -32
- package/ios/Sources/CapMatrixPlugin/CapMatrixPlugin.swift +70 -3
- package/package.json +1 -1
|
@@ -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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
}
|