mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Enable OIDC token refresh in the NSE. (#1711)
Update the SDK and handle API changes.
This commit is contained in:
@@ -5645,7 +5645,7 @@
|
|||||||
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
|
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 1.1.13;
|
version = 1.1.14;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = {
|
821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = {
|
||||||
|
|||||||
@@ -129,8 +129,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
|
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "482c8c04019e6e6ac9638ae792adae9d67b08114",
|
"revision" : "db9a4c6eddc385c62695afdf7562827c7a586e72",
|
||||||
"version" : "1.1.13"
|
"version" : "1.1.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
{
|
{
|
||||||
"identity" : "swiftui-introspect",
|
"identity" : "swiftui-introspect",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
"location" : "https://github.com/siteline/SwiftUI-Introspect",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290",
|
"revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290",
|
||||||
"version" : "0.9.2"
|
"version" : "0.9.2"
|
||||||
|
|||||||
@@ -527,8 +527,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
|||||||
switch callback {
|
switch callback {
|
||||||
case .didReceiveAuthError(let isSoftLogout):
|
case .didReceiveAuthError(let isSoftLogout):
|
||||||
stateMachine.processEvent(.signOut(isSoft: isSoftLogout))
|
stateMachine.processEvent(.signOut(isSoft: isSoftLogout))
|
||||||
case .updateRestorationToken:
|
|
||||||
userSessionStore.refreshRestorationToken(for: userSession)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
|||||||
passphrase: nil,
|
passphrase: nil,
|
||||||
userAgent: UserAgentBuilder.makeASCIIUserAgent(),
|
userAgent: UserAgentBuilder.makeASCIIUserAgent(),
|
||||||
oidcConfiguration: oidcConfiguration,
|
oidcConfiguration: oidcConfiguration,
|
||||||
customSlidingSyncProxy: appSettings.slidingSyncProxyURL?.absoluteString)
|
customSlidingSyncProxy: appSettings.slidingSyncProxyURL?.absoluteString,
|
||||||
|
sessionDelegate: userSessionStore.clientSessionDelegate,
|
||||||
|
crossProcessRefreshLockId: InfoPlistReader.main.bundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
|
client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
|
||||||
self?.hasEncounteredAuthError = true
|
self?.hasEncounteredAuthError = true
|
||||||
self?.callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
self?.callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||||
} tokenRefreshCallback: { [weak self] in
|
|
||||||
self?.callbacks.send(.updateRestorationToken)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await configureAppService()
|
await configureAppService()
|
||||||
@@ -536,12 +534,9 @@ private class RoomListServiceSyncIndicatorListenerProxy: RoomListServiceSyncIndi
|
|||||||
|
|
||||||
private class ClientDelegateWrapper: ClientDelegate {
|
private class ClientDelegateWrapper: ClientDelegate {
|
||||||
private let authErrorCallback: (Bool) -> Void
|
private let authErrorCallback: (Bool) -> Void
|
||||||
private let tokenRefreshCallback: () -> Void
|
|
||||||
|
|
||||||
init(authErrorCallback: @escaping (Bool) -> Void,
|
init(authErrorCallback: @escaping (Bool) -> Void) {
|
||||||
tokenRefreshCallback: @escaping () -> Void) {
|
|
||||||
self.authErrorCallback = authErrorCallback
|
self.authErrorCallback = authErrorCallback
|
||||||
self.tokenRefreshCallback = tokenRefreshCallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ClientDelegate
|
// MARK: - ClientDelegate
|
||||||
@@ -552,7 +547,6 @@ private class ClientDelegateWrapper: ClientDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func didRefreshTokens() {
|
func didRefreshTokens() {
|
||||||
MXLog.info("The session has updated tokens.")
|
MXLog.info("Delegating session updates to the ClientSessionDelegate.")
|
||||||
tokenRefreshCallback()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import MatrixRustSDK
|
|||||||
enum ClientProxyCallback {
|
enum ClientProxyCallback {
|
||||||
case receivedSyncUpdate
|
case receivedSyncUpdate
|
||||||
case receivedAuthError(isSoftLogout: Bool)
|
case receivedAuthError(isSoftLogout: Bool)
|
||||||
case updateRestorationToken
|
|
||||||
|
|
||||||
var isSyncUpdate: Bool {
|
var isSyncUpdate: Bool {
|
||||||
if case .receivedSyncUpdate = self {
|
if case .receivedSyncUpdate = self {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import KeychainAccess
|
import KeychainAccess
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
enum KeychainControllerService: String {
|
enum KeychainControllerService: String {
|
||||||
case sessions
|
case sessions
|
||||||
@@ -86,4 +87,20 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
MXLog.error("Failed removing all tokens")
|
MXLog.error("Failed removing all tokens")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ClientSessionDelegate
|
||||||
|
|
||||||
|
func retrieveSessionFromKeychain(userId: String) throws -> Session {
|
||||||
|
MXLog.info("Retrieving an updated Session from the keychain.")
|
||||||
|
guard let session = restorationTokenForUsername(userId)?.session else {
|
||||||
|
throw ClientError.Generic(msg: "Failed to find RestorationToken in the Keychain.")
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSessionInKeychain(session: Session) {
|
||||||
|
MXLog.info("Saving session changes in the keychain.")
|
||||||
|
let restorationToken = RestorationToken(session: session)
|
||||||
|
setRestorationToken(restorationToken, forUsername: session.userId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct KeychainCredentials {
|
struct KeychainCredentials {
|
||||||
let userID: String
|
let userID: String
|
||||||
let restorationToken: RestorationToken
|
let restorationToken: RestorationToken
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol KeychainControllerProtocol {
|
protocol KeychainControllerProtocol: ClientSessionDelegate {
|
||||||
func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String)
|
func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String)
|
||||||
func restorationTokenForUsername(_ username: String) -> RestorationToken?
|
func restorationTokenForUsername(_ username: String) -> RestorationToken?
|
||||||
func restorationTokens() -> [KeychainCredentials]
|
func restorationTokens() -> [KeychainCredentials]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ extension NotificationItemProxyProtocol {
|
|||||||
return false
|
return false
|
||||||
case let .messageLike(content):
|
case let .messageLike(content):
|
||||||
switch content {
|
switch content {
|
||||||
case let .roomMessage(messageType):
|
case let .roomMessage(messageType, _):
|
||||||
switch messageType {
|
switch messageType {
|
||||||
case .image, .video, .audio:
|
case .image, .video, .audio:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
inviterProxy = RoomMemberProxy(member: inviter, backgroundTaskService: backgroundTaskService)
|
inviterProxy = RoomMemberProxy(member: inviter, backgroundTaskService: backgroundTaskService)
|
||||||
}
|
}
|
||||||
|
|
||||||
let notificationMode = roomInfo.notificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
let notificationMode = roomInfo.userDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
||||||
|
|
||||||
let details = RoomSummaryDetails(id: roomInfo.id,
|
let details = RoomSummaryDetails(id: roomInfo.id,
|
||||||
name: roomInfo.name ?? roomInfo.id,
|
name: roomInfo.name ?? roomInfo.id,
|
||||||
|
|||||||
@@ -94,8 +94,6 @@ class UserSession: UserSessionProtocol {
|
|||||||
case .receivedAuthError(let isSoftLogout):
|
case .receivedAuthError(let isSoftLogout):
|
||||||
callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout))
|
callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout))
|
||||||
tearDownAuthErrorWatchdog()
|
tearDownAuthErrorWatchdog()
|
||||||
case .updateRestorationToken:
|
|
||||||
callbacks.send(.updateRestorationToken)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ enum UserSessionCallback {
|
|||||||
case sessionVerificationNeeded
|
case sessionVerificationNeeded
|
||||||
case didVerifySession
|
case didVerifySession
|
||||||
case didReceiveAuthError(isSoftLogout: Bool)
|
case didReceiveAuthError(isSoftLogout: Bool)
|
||||||
case updateRestorationToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol UserSessionProtocol {
|
protocol UserSessionProtocol {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
/// The base directory where all session data is stored.
|
/// The base directory where all session data is stored.
|
||||||
let baseDirectory: URL
|
let baseDirectory: URL
|
||||||
|
|
||||||
|
var clientSessionDelegate: ClientSessionDelegate { keychainController }
|
||||||
|
|
||||||
init(backgroundTaskService: BackgroundTaskServiceProtocol) {
|
init(backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
keychainController = KeychainController(service: .sessions,
|
keychainController = KeychainController(service: .sessions,
|
||||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||||
@@ -76,16 +78,6 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
return .failure(error)
|
return .failure(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshRestorationToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError> {
|
|
||||||
guard let restorationToken = userSession.clientProxy.restorationToken else {
|
|
||||||
return .failure(.failedRefreshingRestoreToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userID)
|
|
||||||
|
|
||||||
return .success(())
|
|
||||||
}
|
|
||||||
|
|
||||||
func logout(userSession: UserSessionProtocol) {
|
func logout(userSession: UserSessionProtocol) {
|
||||||
let userID = userSession.clientProxy.userID
|
let userID = userSession.clientProxy.userID
|
||||||
@@ -115,6 +107,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
.username(username: credentials.userID)
|
.username(username: credentials.userID)
|
||||||
.homeserverUrl(url: credentials.restorationToken.session.homeserverUrl)
|
.homeserverUrl(url: credentials.restorationToken.session.homeserverUrl)
|
||||||
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
|
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
|
||||||
|
.enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier,
|
||||||
|
sessionDelegate: keychainController)
|
||||||
.serverVersions(versions: ["v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"]) // FIXME: Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
|
.serverVersions(versions: ["v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"]) // FIXME: Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -133,9 +127,9 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||||
do {
|
do {
|
||||||
let session = try client.session()
|
let session = try client.session()
|
||||||
let userId = try client.userId()
|
let userID = try client.userId()
|
||||||
|
|
||||||
keychainController.setRestorationToken(RestorationToken(session: session), forUsername: userId)
|
keychainController.setRestorationToken(RestorationToken(session: session), forUsername: userID)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed setting up user session with error: \(error)")
|
MXLog.error("Failed setting up user session with error: \(error)")
|
||||||
return .failure(.failedSettingUpSession)
|
return .failure(.failedSettingUpSession)
|
||||||
|
|||||||
@@ -37,15 +37,14 @@ protocol UserSessionStoreProtocol {
|
|||||||
/// Returns the location to store user data for a particular username.
|
/// Returns the location to store user data for a particular username.
|
||||||
var baseDirectory: URL { get }
|
var baseDirectory: URL { get }
|
||||||
|
|
||||||
|
/// Returns the delegate that should handle any changes to a `Client`'s `Session`.
|
||||||
|
var clientSessionDelegate: ClientSessionDelegate { get }
|
||||||
|
|
||||||
/// Restores an existing user session.
|
/// Restores an existing user session.
|
||||||
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
|
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||||
|
|
||||||
/// Creates a user session for a new client from the SDK.
|
/// Creates a user session for a new client from the SDK.
|
||||||
func userSession(for client: Client) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
func userSession(for client: Client) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||||
|
|
||||||
/// Refresh the restore token of the client for a given session.
|
|
||||||
@discardableResult
|
|
||||||
func refreshRestorationToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError>
|
|
||||||
|
|
||||||
/// Logs out of the specified session.
|
/// Logs out of the specified session.
|
||||||
func logout(userSession: UserSessionProtocol)
|
func logout(userSession: UserSessionProtocol)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ struct NotificationContentBuilder {
|
|||||||
switch try? event.eventType() {
|
switch try? event.eventType() {
|
||||||
case let .messageLike(content):
|
case let .messageLike(content):
|
||||||
switch content {
|
switch content {
|
||||||
case .roomMessage(messageType: let messageType):
|
case .roomMessage(let messageType, _):
|
||||||
return try await processRoomMessage(notificationItem: notificationItem, messageType: messageType, mediaProvider: mediaProvider)
|
return try await processRoomMessage(notificationItem: notificationItem, messageType: messageType, mediaProvider: mediaProvider)
|
||||||
default:
|
default:
|
||||||
return processEmpty(notificationItem: notificationItem)
|
return processEmpty(notificationItem: notificationItem)
|
||||||
|
|||||||
@@ -41,12 +41,6 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
// - NotificationID could not be resolved
|
// - NotificationID could not be resolved
|
||||||
return contentHandler(request.content)
|
return contentHandler(request.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
if credentials.restorationToken.session.oidcData != nil {
|
|
||||||
// Notification content is disabled for OIDC sessions
|
|
||||||
// until token refresh is multi-process aware.
|
|
||||||
return contentHandler(request.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler = contentHandler
|
handler = contentHandler
|
||||||
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
||||||
@@ -80,8 +74,9 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)")
|
MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let userSession = try NSEUserSession(credentials: credentials)
|
let userSession = try NSEUserSession(credentials: credentials, clientSessionDelegate: keychainController)
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
|
|
||||||
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else {
|
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else {
|
||||||
MXLog.info("\(tag) no notification for the event, discard")
|
MXLog.info("\(tag) no notification for the event, discard")
|
||||||
return discard()
|
return discard()
|
||||||
|
|||||||
@@ -25,27 +25,30 @@ final class NSEUserSession {
|
|||||||
imageCache: .onlyOnDisk,
|
imageCache: .onlyOnDisk,
|
||||||
backgroundTaskService: nil)
|
backgroundTaskService: nil)
|
||||||
|
|
||||||
init(credentials: KeychainCredentials) throws {
|
init(credentials: KeychainCredentials, clientSessionDelegate: ClientSessionDelegate) throws {
|
||||||
userID = credentials.userID
|
userID = credentials.userID
|
||||||
baseClient = try ClientBuilder()
|
baseClient = try ClientBuilder()
|
||||||
.basePath(path: URL.sessionsBaseDirectory.path)
|
.basePath(path: URL.sessionsBaseDirectory.path)
|
||||||
.username(username: credentials.userID)
|
.username(username: credentials.userID)
|
||||||
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
|
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
|
||||||
|
.enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier,
|
||||||
|
sessionDelegate: clientSessionDelegate)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
baseClient.setDelegate(delegate: ClientDelegateWrapper())
|
||||||
try baseClient.restoreSession(session: credentials.restorationToken.session)
|
try baseClient.restoreSession(session: credentials.restorationToken.session)
|
||||||
|
|
||||||
notificationClient = try baseClient
|
notificationClient = try baseClient
|
||||||
.notificationClient(processSetup: .multipleProcesses)
|
.notificationClient(processSetup: .multipleProcesses)
|
||||||
.filterByPushRules()
|
.filterByPushRules()
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? {
|
func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? {
|
||||||
await Task.dispatch(on: .global()) {
|
await Task.dispatch(on: .global()) {
|
||||||
do {
|
do {
|
||||||
let notification = try self.notificationClient.getNotification(roomId: roomID, eventId: eventID)
|
let notification = try self.notificationClient.getNotification(roomId: roomID, eventId: eventID)
|
||||||
|
|
||||||
guard let notification else {
|
guard let notification else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -60,3 +63,15 @@ final class NSEUserSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ClientDelegateWrapper: ClientDelegate {
|
||||||
|
// MARK: - ClientDelegate
|
||||||
|
|
||||||
|
func didReceiveAuthError(isSoftLogout: Bool) {
|
||||||
|
MXLog.error("Received authentication error, the NSE can't handle this.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func didRefreshTokens() {
|
||||||
|
MXLog.info("Delegating session updates to the ClientSessionDelegate.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Enable token refresh in the NSE (and notifications for OIDC accounts).
|
||||||
+1
-1
@@ -46,7 +46,7 @@ packages:
|
|||||||
# Element/Matrix dependencies
|
# Element/Matrix dependencies
|
||||||
MatrixRustSDK:
|
MatrixRustSDK:
|
||||||
url: https://github.com/matrix-org/matrix-rust-components-swift
|
url: https://github.com/matrix-org/matrix-rust-components-swift
|
||||||
exactVersion: 1.1.13
|
exactVersion: 1.1.14
|
||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
DesignKit:
|
DesignKit:
|
||||||
path: DesignKit
|
path: DesignKit
|
||||||
|
|||||||
Reference in New Issue
Block a user