First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import config.BuildTimeConfig
import extension.buildConfigFieldStr
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "io.element.android.libraries.matrix.api"
buildFeatures {
buildConfig = true
}
defaultConfig {
buildConfigFieldStr(
name = "CLIENT_URI",
value = BuildTimeConfig.URL_WEBSITE ?: "https://element.io"
)
buildConfigFieldStr(
name = "LOGO_URI",
value = BuildTimeConfig.URL_LOGO ?: "https://element.io/mobile-icon.png"
)
buildConfigFieldStr(
name = "TOS_URI",
value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms"
)
buildConfigFieldStr(
name = "POLICY_URI",
value = BuildTimeConfig.URL_POLICY ?: "https://element.io/privacy"
)
}
}
dependencies {
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.services.analytics.api)
implementation(libs.serialization.json)
api(projects.libraries.sessionStorage.api)
implementation(libs.coroutines.core)
api(projects.libraries.architecture)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,208 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.NotJoinedRoom
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.util.Optional
interface MatrixClient {
val sessionId: SessionId
val deviceId: DeviceId
val userProfile: StateFlow<MatrixUser>
val roomListService: RoomListService
val spaceService: SpaceService
val syncService: SyncService
val sessionVerificationService: SessionVerificationService
val pushersService: PushersService
val notificationService: NotificationService
val notificationSettingsService: NotificationSettingsService
val encryptionService: EncryptionService
val roomDirectoryService: RoomDirectoryService
val mediaPreviewService: MediaPreviewService
val matrixMediaLoader: MatrixMediaLoader
val sessionCoroutineScope: CoroutineScope
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
val roomMembershipObserver: RoomMembershipObserver
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
suspend fun getRoom(roomId: RoomId): BaseRoom?
suspend fun findDM(userId: UserId): Result<RoomId?>
suspend fun getJoinedRoomIds(): Result<Set<RoomId>>
suspend fun ignoreUser(userId: UserId): Result<Unit>
suspend fun unignoreUser(userId: UserId): Result<Unit>
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
suspend fun createDM(userId: UserId): Result<RoomId>
suspend fun getProfile(userId: UserId): Result<MatrixUser>
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
suspend fun setDisplayName(displayName: String): Result<Unit>
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit>
suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?>
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomInfo?>
suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomInfo?>
suspend fun getCacheSize(): Long
/**
* Will close the client and delete the cache data.
*/
suspend fun clearCache()
/**
* Logout the user.
*
* @param userInitiated if false, the logout came from the HS, no request will be made and the session entry will be kept in the store.
* @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway.
*/
suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean)
/**
* Retrieve the user profile, will also eventually emit a new value to [userProfile].
*/
suspend fun getUserProfile(): Result<MatrixUser>
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String>
/**
* Get a room info flow for a given room ID.
* The flow will emit a new value whenever the room info is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<RoomInfo>>
fun isMe(userId: UserId?) = userId == sessionId
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
/**
* Resolves the given room alias to a roomID (and a list of servers), if possible.
* @param roomAlias the room alias to resolve
* @return the resolved room alias if any, an empty result if not found,or an error if the resolution failed.
*
*/
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<Optional<ResolvedRoomAlias>>
/**
* Enables or disables the sending queue, according to the given parameter.
*
* The sending queue automatically disables itself whenever sending an
* event with it failed (e.g. sending an event via the Timeline),
* so it's required to manually re-enable it as soon as
* connectivity is back on the device.
*/
suspend fun setAllSendQueuesEnabled(enabled: Boolean)
/**
* Returns a flow of room IDs that have send queue being disabled.
* This flow will emit a new value whenever the send queue is disabled for a room.
*/
fun sendQueueDisabledFlow(): Flow<RoomId>
/**
* Return the server name part of the current user ID, using the SDK, and if a failure occurs,
* compute it manually.
*/
fun userIdServerName(): String
/**
* Execute generic GET requests through the SDKs internal HTTP client.
*/
suspend fun getUrl(url: String): Result<ByteArray>
/**
* Get a room preview for a given room ID or alias. This is especially useful for rooms that the user is not a member of, or hasn't joined yet.
*/
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<NotJoinedRoom>
/**
* Returns the currently used sliding sync version.
*/
suspend fun currentSlidingSyncVersion(): Result<SlidingSyncVersion>
fun canDeactivateAccount(): Boolean
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
/**
* Check if the user can report a room.
*/
suspend fun canReportRoom(): Boolean
/**
* Return true if Livekit Rtc is supported, i.e. if Element Call is available.
*/
suspend fun isLivekitRtcSupported(): Boolean
/**
* Returns the maximum file upload size allowed by the Matrix server.
*/
suspend fun getMaxFileUploadSize(): Result<Long>
/**
* Returns the list of shared recent emoji reactions for this account.
*/
suspend fun getRecentEmojis(): Result<List<String>>
/**
* Adds an emoji to the list of recent emoji reactions for this account.
*/
suspend fun addRecentEmoji(emoji: String): Result<Unit>
/**
* Marks the room with the provided [roomId] as read, sending a fully read receipt for [eventId].
*
* This method should be used with caution as providing the [eventId] ourselves can result in incorrect read receipts.
* Use [Timeline.markAsRead] instead when possible.
*/
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
}
/**
* Returns a room alias from a room alias name, or null if the name is not valid.
* @param name the room alias name ie. the local part of the room alias.
*/
fun MatrixClient.roomAliasFromName(name: String): RoomAlias? {
return name.takeIf { it.isNotEmpty() }
?.let { "#$it:${userIdServerName()}" }
?.takeIf { MatrixPatterns.isRoomAlias(it) }
?.let { tryOrNull { RoomAlias(it) } }
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.SessionId
interface MatrixClientProvider {
/**
* Can be used to get or restore a MatrixClient with the given [SessionId].
* If a [MatrixClient] is already in memory, it'll return it. Otherwise it'll try to restore one.
* Most of the time you want to use injected constructor instead of retrieving a MatrixClient with this provider.
*/
suspend fun getOrRestore(sessionId: SessionId): Result<MatrixClient>
/**
* Can be used to retrieve an existing [MatrixClient] with the given [SessionId].
* @param sessionId the [SessionId] of the [MatrixClient] to retrieve.
* @return the [MatrixClient] if it exists.
*/
fun getOrNull(sessionId: SessionId): MatrixClient?
}
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api
interface SdkMetadata {
val sdkGitSha: String
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.analytics
import im.vector.app.features.analytics.plan.ViewRoom
import io.element.android.libraries.matrix.api.room.BaseRoom
fun BaseRoom.toAnalyticsViewRoom(
trigger: ViewRoom.Trigger? = null,
selectedSpace: BaseRoom? = null,
viaKeyboard: Boolean? = null,
): ViewRoom {
val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home
return ViewRoom(
isDM = info().isDirect,
isSpace = info().isSpace,
trigger = trigger,
activeSpace = activeSpace,
viaKeyboard = viaKeyboard
)
}
private fun BaseRoom.toActiveSpace(): ViewRoom.ActiveSpace {
return if (info().isPublic == true) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
enum class AuthErrorCode(val value: String) {
UNKNOWN("M_UNKNOWN"),
USER_DEACTIVATED("M_USER_DEACTIVATED"),
FORBIDDEN("M_FORBIDDEN")
}
// This is taken from the iOS version. It seems like currently there's no better way to extract error codes
val AuthenticationException.errorCode: AuthErrorCode
get() {
val message = (this as? AuthenticationException.Generic)?.message ?: return AuthErrorCode.UNKNOWN
return enumValues<AuthErrorCode>()
.firstOrNull { message.contains(it.value) }
?: AuthErrorCode.UNKNOWN
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
sealed class AuthenticationException(message: String?) : Exception(message) {
data class AccountAlreadyLoggedIn(
val userId: String,
) : AuthenticationException(null)
class InvalidServerName(message: String?) : AuthenticationException(message)
class SlidingSyncVersion(message: String?) : AuthenticationException(message)
class ServerUnreachable(message: String?) : AuthenticationException(message)
class Oidc(message: String?) : AuthenticationException(message)
class Generic(message: String?) : AuthenticationException(message)
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
/**
* Checks the homeserver's compatibility with Element X.
*/
interface HomeServerLoginCompatibilityChecker {
/**
* Performs the compatibility check given the homeserver's [url].
* @return a `true` value if the homeserver is compatible, `false` if not, or a failure result if the check unexpectedly failed.
*/
suspend fun check(url: String): Result<Boolean>
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
interface MatrixAuthenticationService {
/**
* Restore a session from a [sessionId].
* Do not restore anything it the access token is not valid anymore.
* Generally this method should not be used directly, prefer using [MatrixClientProvider.getOrRestore] instead.
*/
suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient>
/**
* Set the homeserver to use for authentication, and return its details.
*/
suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails>
suspend fun login(username: String, password: String): Result<SessionId>
/**
* Import a session that was created using another client, for instance Element Web.
*/
suspend fun importCreatedSession(externalSession: ExternalSession): Result<SessionId>
/*
* OIDC part.
*/
/**
* Get the Oidc url to display to the user.
*/
suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails>
/**
* Cancel Oidc login sequence.
*/
suspend fun cancelOidcLogin(): Result<Unit>
/**
* Attempt to login using the [callbackUrl] provided by the Oidc page.
*/
suspend fun loginWithOidc(callbackUrl: String): Result<SessionId>
suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result<SessionId>
/** Listen to new Matrix clients being created on authentication. */
fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit)
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
data class MatrixHomeServerDetails(
val url: String,
val supportsPasswordLogin: Boolean,
val supportsOidcLogin: Boolean,
) {
val isSupported = supportsPasswordLogin || supportsOidcLogin
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
import io.element.android.libraries.matrix.api.BuildConfig
object OidcConfig {
const val CLIENT_URI = BuildConfig.CLIENT_URI
// Note: host must match with the host of CLIENT_URI
const val LOGO_URI = BuildConfig.LOGO_URI
// Note: host must match with the host of CLIENT_URI
const val TOS_URI = BuildConfig.TOS_URI
// Note: host must match with the host of CLIENT_URI
const val POLICY_URI = BuildConfig.POLICY_URI
// Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
val STATIC_REGISTRATIONS = mapOf(
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
)
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class OidcDetails(
val url: String,
) : Parcelable
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
sealed interface OidcPrompt {
/**
* The Authorization Server should prompt the End-User for
* reauthentication.
*/
data object Login : OidcPrompt
/**
* The Authorization Server should prompt the End-User to create a user
* account.
*
* Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
*/
data object Create : OidcPrompt
/**
* An unknown value.
*/
data class Unknown(val value: String) : OidcPrompt
}
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
interface OidcRedirectUrlProvider {
fun provide(): String
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth.external
/***
* Represents a session data of a session created by another client.
*/
data class ExternalSession(
val userId: String,
val deviceId: String,
val accessToken: String,
val refreshToken: String?,
val homeserverUrl: String,
)
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth.qrlogin
interface MatrixQrCodeLoginData {
fun serverName(): String?
}
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth.qrlogin
interface MatrixQrCodeLoginDataFactory {
fun parseQrCodeData(data: ByteArray): Result<MatrixQrCodeLoginData>
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth.qrlogin
sealed class QrCodeDecodeException(message: String) : Exception(message) {
class Crypto(
message: String,
// val reason: Reason
) : QrCodeDecodeException(message) {
// We plan to restore it in the future when UniFFi can process them
// enum class Reason {
// NOT_ENOUGH_DATA,
// NOT_UTF8,
// URL_PARSE,
// INVALID_MODE,
// INVALID_VERSION,
// BASE64,
// INVALID_PREFIX
// }
}
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth.qrlogin
sealed interface QrCodeLoginStep {
data object Uninitialized : QrCodeLoginStep
data class EstablishingSecureChannel(val checkCode: String) : QrCodeLoginStep
data object Starting : QrCodeLoginStep
data class WaitingForToken(val userCode: String) : QrCodeLoginStep
data object SyncingSecrets : QrCodeLoginStep
data class Failed(val error: QrLoginException) : QrCodeLoginStep
data object Finished : QrCodeLoginStep
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth.qrlogin
sealed class QrLoginException : Exception() {
data object Cancelled : QrLoginException()
data object ConnectionInsecure : QrLoginException()
data object Declined : QrLoginException()
data object Expired : QrLoginException()
data object LinkingNotSupported : QrLoginException()
data object OidcMetadataInvalid : QrLoginException()
data object SlidingSyncNotAvailable : QrLoginException()
data object OtherDeviceNotSignedIn : QrLoginException()
data object CheckCodeAlreadySent : QrLoginException()
data object CheckCodeCannotBeSent : QrLoginException()
data object Unknown : QrLoginException()
data object NotFound : QrLoginException()
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import java.io.Serializable
@JvmInline
value class DeviceId(val value: String) : Serializable {
override fun toString(): String = value
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.androidutils.metadata.isInDebug
import java.io.Serializable
@JvmInline
value class EventId(val value: String) : Serializable {
init {
if (isInDebug && !MatrixPatterns.isEventId(value)) {
error("`$value` is not a valid event id.\nExample event id: `\$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg`.")
}
}
override fun toString(): String = value
}
fun EventId.toThreadId(): ThreadId = ThreadId(value)
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import java.io.Serializable
@JvmInline
value class FlowId(val value: String) : Serializable {
override fun toString(): String = value
}
@@ -0,0 +1,176 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
/**
* This class contains pattern to match the different Matrix ids
* Ref: https://matrix.org/docs/spec/appendices#identifier-grammar
*/
object MatrixPatterns {
// Note: TLD is not mandatory (localhost, IP address...)
private const val DOMAIN_REGEX = ":[A-Za-z0-9.-]+(:[0-9]{2,5})?"
private const val BASE_64_ALPHABET = "[0-9A-Za-z/\\+=]+"
private const val BASE_64_URL_SAFE_ALPHABET = "[0-9A-Za-z/\\-_]+"
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
// Sadly, we need to relax the regex pattern a bit as there already exist some ids that don't match the spec.
// Note: local part can be empty
private const val MATRIX_USER_IDENTIFIER_REGEX = "^@\\S*?$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex()
// !localpart:domain" used in most room versions prior to MSC4291
// Note: roomId can be arbitrary strings, including space and new line char
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "^!.+$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.DOT_MATCHES_ALL)
// "!event_id_base_64" used in room versions post MSC4291
private const val MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX = "!$BASE_64_URL_SAFE_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS = MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX.toRegex()
// regex pattern to match room aliases.
private const val MATRIX_ROOM_ALIAS_REGEX = "^#\\S+$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to match event ids.
// Sadly, we need to relax the regex pattern a bit as there already exist some ids that don't match the spec.
// v1 and v2: arbitrary string + domain
private const val MATRIX_EVENT_IDENTIFIER_REGEX = "^\\$.+$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex()
// v3: base64
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$$BASE_64_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex()
// v4: url-safe base64
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$$BASE_64_URL_SAFE_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex()
private const val MAX_IDENTIFIER_LENGTH = 255
/**
* Tells if a string is a valid user Id.
*
* @param str the string to test
* @return true if the string is a valid user id
*/
fun isUserId(str: String?): Boolean {
return str != null &&
str.length <= MAX_IDENTIFIER_LENGTH &&
str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
}
/**
* Tells if a string is a valid room id.
*
* @param str the string to test
* @return true if the string is a valid room Id
*/
fun isRoomId(str: String?): Boolean {
return str != null &&
str.length <= MAX_IDENTIFIER_LENGTH &&
(str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS ||
str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER)
}
/**
* Tells if a string is a valid room alias.
*
* @param str the string to test
* @return true if the string is a valid room alias.
*/
fun isRoomAlias(str: String?): Boolean {
return str != null &&
str.length <= MAX_IDENTIFIER_LENGTH &&
str matches PATTERN_CONTAIN_MATRIX_ALIAS
}
/**
* Tells if a string is a valid event id.
*
* @param str the string to test
* @return true if the string is a valid event id.
*/
fun isEventId(str: String?): Boolean {
return str != null &&
str.length <= MAX_IDENTIFIER_LENGTH &&
(str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 ||
str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 ||
str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER)
}
/**
* Tells if a string is a valid thread id. This is an alias for [isEventId].
*
* @param str the string to test
* @return true if the string is a valid thread id.
*/
fun isThreadId(str: String?) = isEventId(str)
/**
* Finds existing ids or aliases in a [CharSequence].
* Note not all cases are implemented.
*/
fun findPatterns(text: CharSequence, permalinkParser: PermalinkParser): List<MatrixPatternResult> {
val rawTextMatches = "\\S+$DOMAIN_REGEX".toRegex(RegexOption.IGNORE_CASE).findAll(text)
val urlMatches = "\\[\\S+\\]\\((\\S+)\\)".toRegex(RegexOption.IGNORE_CASE).findAll(text)
val atRoomMatches = Regex("@room").findAll(text)
return buildList {
for (match in rawTextMatches) {
// Match existing id and alias patterns in the text
val type = when {
isUserId(match.value) -> MatrixPatternType.USER_ID
isRoomId(match.value) -> MatrixPatternType.ROOM_ID
isRoomAlias(match.value) -> MatrixPatternType.ROOM_ALIAS
isEventId(match.value) -> MatrixPatternType.EVENT_ID
else -> null
}
if (type != null) {
add(MatrixPatternResult(type, match.value, match.range.first, match.range.last + 1))
}
}
for (match in urlMatches) {
// Extract the link and check if it's a valid permalink
val urlMatch = match.groupValues[1]
when (val permalink = permalinkParser.parse(urlMatch)) {
is PermalinkData.UserLink -> {
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.value, match.range.first, match.range.last + 1))
}
is PermalinkData.RoomLink -> {
when (permalink.roomIdOrAlias) {
is RoomIdOrAlias.Alias -> MatrixPatternType.ROOM_ALIAS
is RoomIdOrAlias.Id -> if (permalink.eventId == null) MatrixPatternType.ROOM_ID else null
}?.let { type ->
add(MatrixPatternResult(type, permalink.roomIdOrAlias.identifier, match.range.first, match.range.last + 1))
}
}
else -> Unit
}
}
for (match in atRoomMatches) {
// Special case for `@room` mentions
add(MatrixPatternResult(MatrixPatternType.AT_ROOM, match.value, match.range.first, match.range.last + 1))
}
}
}
}
enum class MatrixPatternType {
USER_ID,
ROOM_ID,
ROOM_ALIAS,
EVENT_ID,
AT_ROOM
}
data class MatrixPatternResult(val type: MatrixPatternType, val value: String, val start: Int, val end: Int)
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
interface ProgressCallback {
fun onProgress(current: Long, total: Long)
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.androidutils.metadata.isInDebug
import java.io.Serializable
@JvmInline
value class RoomAlias(val value: String) : Serializable {
init {
if (isInDebug && !MatrixPatterns.isRoomAlias(value)) {
error("`$value` is not a valid room alias.\n Example room alias: `#room_alias:domain`.")
}
}
override fun toString(): String = value
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.androidutils.metadata.isInDebug
import java.io.Serializable
@JvmInline
value class RoomId(val value: String) : Serializable {
init {
if (isInDebug && !MatrixPatterns.isRoomId(value)) {
error("`$value` is not a valid room id.\n Example room id: `!room_id:domain`.")
}
}
override fun toString(): String = value
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
@Immutable
sealed interface RoomIdOrAlias : Parcelable {
@Parcelize
@JvmInline
value class Id(val roomId: RoomId) : RoomIdOrAlias
@Parcelize
@JvmInline
value class Alias(val roomAlias: RoomAlias) : RoomIdOrAlias
val identifier: String
get() = when (this) {
is Id -> roomId.value
is Alias -> roomAlias.value
}
}
fun RoomId.toRoomIdOrAlias() = RoomIdOrAlias.Id(this)
fun RoomAlias.toRoomIdOrAlias() = RoomIdOrAlias.Alias(this)
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
fun interface SendHandle {
suspend fun retry(): Result<Unit>
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
/**
* The [UserId] of the currently logged in user.
*/
typealias SessionId = UserId
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
typealias SpaceId = RoomId
/**
* Value to use when no space is selected by the user.
*/
val MAIN_SPACE = SpaceId("!mainSpace:local")
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.androidutils.metadata.isInDebug
import java.io.Serializable
@JvmInline
value class ThreadId(val value: String) : Serializable {
init {
if (isInDebug && !MatrixPatterns.isThreadId(value)) {
error(
"`$value` is not a valid thread id.\n" +
"Thread ids are the same as event ids.\n" +
"Example thread id: `\$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg`."
)
}
}
override fun toString(): String = value
}
fun ThreadId.asEventId(): EventId = EventId(value)
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import java.io.Serializable
@JvmInline
value class TransactionId(val value: String) : Serializable {
override fun toString(): String = value
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import java.io.Serializable
@JvmInline
value class UniqueId(val value: String) : Serializable {
override fun toString(): String = value
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import io.element.android.libraries.androidutils.metadata.isInDebug
import java.io.Serializable
/**
* A [String] holding a valid Matrix user ID.
*
* https://spec.matrix.org/v1.8/appendices/#user-identifiers
*/
@JvmInline
value class UserId(val value: String) : Serializable {
init {
if (isInDebug && !MatrixPatterns.isUserId(value)) {
error("`$value` is not a valid user id.\nExample user id: `@name:domain`.")
}
}
override fun toString(): String = value
val extractedDisplayName: String
get() = value
.removePrefix("@")
.substringBefore(":")
val domainName: String?
get() = value.substringAfter(":").takeIf { it.isNotEmpty() }
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.createroom
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import java.util.Optional
data class CreateRoomParameters(
val name: String?,
val topic: String? = null,
val isEncrypted: Boolean,
val isDirect: Boolean = false,
val visibility: RoomVisibility,
val preset: RoomPreset,
val invite: List<UserId>? = null,
val avatar: String? = null,
val joinRuleOverride: JoinRule? = null,
val historyVisibilityOverride: RoomHistoryVisibility? = null,
val roomAliasName: Optional<String> = Optional.empty(),
)
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.createroom
enum class RoomPreset {
PRIVATE_CHAT,
PUBLIC_CHAT,
TRUSTED_PRIVATE_CHAT,
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
enum class BackupState {
/**
* Special value, when the SDK is waiting for the first sync to be done.
*/
WAITING_FOR_SYNC,
/**
* Values mapped from the SDK.
*/
UNKNOWN,
CREATING,
ENABLING,
RESUMING,
ENABLED,
DOWNLOADING,
DISABLING
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
import androidx.compose.runtime.Immutable
@Immutable
sealed interface BackupUploadState {
data object Unknown : BackupUploadState
data object Waiting : BackupUploadState
data class Uploading(
val backedUpCount: Int,
val totalCount: Int,
) : BackupUploadState
data object Done : BackupUploadState
data object Error : BackupUploadState
data class SteadyException(val exception: SteadyStateException) : BackupUploadState
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
sealed interface EnableRecoveryProgress {
data object Starting : EnableRecoveryProgress
data object CreatingBackup : EnableRecoveryProgress
data object CreatingRecoveryKey : EnableRecoveryProgress
data class BackingUp(val backedUpCount: Int, val totalCount: Int) : EnableRecoveryProgress
data object RoomKeyUploadError : EnableRecoveryProgress
data class Done(val recoveryKey: String) : EnableRecoveryProgress
}
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface EncryptionService {
val backupStateStateFlow: StateFlow<BackupState>
val recoveryStateStateFlow: StateFlow<RecoveryState>
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
val isLastDevice: StateFlow<Boolean>
val hasDevicesToVerifyAgainst: StateFlow<AsyncData<Boolean>>
suspend fun enableBackups(): Result<Unit>
/**
* Enable recovery. Observe enableProgressStateFlow to get progress and recovery key.
*/
suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result<Unit>
/**
* Change the recovery and return the new recovery key.
*/
suspend fun resetRecoveryKey(): Result<String>
suspend fun disableRecovery(): Result<Unit>
suspend fun doesBackupExistOnServer(): Result<Boolean>
/**
* Note: accept both recoveryKey and passphrase.
*/
suspend fun recover(recoveryKey: String): Result<Unit>
/**
* Wait for backup upload steady state.
*/
fun waitForBackupUploadSteadyState(): Flow<BackupUploadState>
/**
* Get the public curve25519 key of our own device in base64. This is usually what is
* called the identity key of the device.
*/
suspend fun deviceCurve25519(): String?
/**
* Get the public ed25519 key of our own device. This is usually what is
* called the fingerprint of the device.
*/
suspend fun deviceEd25519(): String?
/**
* Starts the identity reset process. This will return a handle that can be used to reset the identity.
*/
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
/**
* Remember this identity, ensuring it does not result in a pin violation.
*/
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
/**
* Withdraw the verification for that user (also pin the identity).
*
* Useful when a user that was verified is not anymore, but it is not
* possible to re-verify immediately. This allows to restore communication by reverting the
* user trust from verified to TOFU verified.
*/
suspend fun withdrawVerification(userId: UserId): Result<Unit>
/**
* Get the identity state of a user, if known.
* @param userId the user id to get the identity for.
* @param fallbackToServer whether to fallback to fetching the identity from the server if not known locally. Defaults to true.
*/
suspend fun getUserIdentity(userId: UserId, fallbackToServer: Boolean = true): Result<IdentityState?>
}
/**
* A handle to reset the user's identity.
*/
sealed interface IdentityResetHandle {
/**
* Cancel the reset process and drops the existing handle in the SDK.
*/
suspend fun cancel()
}
/**
* A handle to reset the user's identity with a password login type.
*/
interface IdentityPasswordResetHandle : IdentityResetHandle {
/**
* Reset the password of the user.
*
* This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
* called, or the identity is reset.
*
* @param password the current password, which will be validated before the process takes place.
*/
suspend fun resetPassword(password: String): Result<Unit>
}
/**
* A handle to reset the user's identity with an OIDC login type.
*/
interface IdentityOidcResetHandle : IdentityResetHandle {
/**
* The URL to open in a webview/custom tab to reset the identity.
*/
val url: String
/**
* Reset the identity using the OIDC flow.
*
* This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
* called, or the identity is reset.
*/
suspend fun resetOidc(): Result<Unit>
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
import io.element.android.libraries.matrix.api.exception.ClientException
sealed class RecoveryException(message: String) : Exception(message) {
class SecretStorage(message: String) : RecoveryException(message)
class Import(message: String) : RecoveryException(message)
data object BackupExistsOnServer : RecoveryException("BackupExistsOnServer")
data class Client(val exception: ClientException) : RecoveryException(exception.message ?: "Unknown error")
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
enum class RecoveryState {
/**
* Special value, when the SDK is waiting for the first sync to be done.
*/
WAITING_FOR_SYNC,
/**
* Values mapped from the SDK.
*/
UNKNOWN,
ENABLED,
DISABLED,
INCOMPLETE,
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption
import androidx.compose.runtime.Immutable
@Immutable
sealed interface SteadyStateException {
/**
* The backup can be deleted.
*/
data class BackupDisabled(val message: String) : SteadyStateException
/**
* The task waiting for notifications coming from the upload task can fall behind so much that it lost some notifications.
*/
data class Lagged(val message: String) : SteadyStateException
/**
* The request(s) to upload the room keys failed.
*/
data class Connection(val message: String) : SteadyStateException
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption.identity
enum class IdentityState {
/** The user is verified with us. */
Verified,
/**
* Either this is the first identity we have seen for this user, or the
* user has acknowledged a change of identity explicitly e.g. by
* clicking OK on a notification.
*/
Pinned,
/**
* The user's identity has changed since it was pinned. The user should be
* notified about this and given the opportunity to acknowledge the
* change, which will make the new identity pinned.
*/
PinViolation,
/**
* The user's identity has changed, and before that it was verified. This
* is a serious problem. The user can either verify again to make this
* identity verified, or withdraw verification to make it pinned.
*/
VerificationViolation,
}
fun IdentityState.isAViolation() = this == IdentityState.PinViolation || this == IdentityState.VerificationViolation
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.encryption.identity
import io.element.android.libraries.matrix.api.core.UserId
data class IdentityStateChange(
val userId: UserId,
val identityState: IdentityState,
)
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.exception
sealed class ClientException(message: String, val details: String?) : Exception(message) {
class Generic(message: String, details: String?) : ClientException(message, details)
class MatrixApi(val kind: ErrorKind, val code: String, message: String, details: String?) : ClientException(message, details)
class Other(message: String) : ClientException(message, null)
}
fun ClientException.isNetworkError(): Boolean {
return this is ClientException.Generic && message?.contains("error sending request for url", ignoreCase = true) == true
}
@@ -0,0 +1,458 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.exception
sealed interface ErrorKind {
/**
* M_BAD_ALIAS
*
* One or more room aliases within the m.room.canonical_alias event do
* not point to the room ID for which the state event is to be sent to.
*
* room aliases: https://spec.matrix.org/latest/client-server-api/#room-aliases
*/
data object BadAlias : ErrorKind
/**
* M_BAD_JSON
*
* The request contained valid JSON, but it was malformed in some way, e.g.
* missing required keys, invalid values for keys.
*/
data object BadJson : ErrorKind
/**
* M_BAD_STATE
*
* The state change requested cannot be performed, such as attempting to
* unban a user who is not banned.
*/
data object BadState : ErrorKind
/**
* M_BAD_STATUS
*
* The application service returned a bad status.
*/
data class BadStatus(
/**
* The HTTP status code of the response.
*/
val status: Int?,
/**
* The body of the response.
*/
val body: String?
) : ErrorKind
/**
* M_CANNOT_LEAVE_SERVER_NOTICE_ROOM
*
* The user is unable to reject an invite to join the server notices
* room.
*
* server notices: https://spec.matrix.org/latest/client-server-api/#server-notices
*/
data object CannotLeaveServerNoticeRoom : ErrorKind
/**
* M_CANNOT_OVERWRITE_MEDIA
*
* The create_content_async endpoint was called with a media ID that
* already has content.
*
*/
data object CannotOverwriteMedia : ErrorKind
/**
* M_CAPTCHA_INVALID
*
* The Captcha provided did not match what was expected.
*/
data object CaptchaInvalid : ErrorKind
/**
* M_CAPTCHA_NEEDED
*
* A Captcha is required to complete the request.
*/
data object CaptchaNeeded : ErrorKind
/**
* M_CONNECTION_FAILED
*
* The connection to the application service failed.
*/
data object ConnectionFailed : ErrorKind
/**
* M_CONNECTION_TIMEOUT
*
* The connection to the application service timed out.
*/
data object ConnectionTimeout : ErrorKind
/**
* M_DUPLICATE_ANNOTATION
*
* The request is an attempt to send a duplicate annotation.
*
* duplicate annotation: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
*/
data object DuplicateAnnotation : ErrorKind
/**
* M_EXCLUSIVE
*
* The resource being requested is reserved by an application service, or
* the application service making the request has not created the
* resource.
*/
data object Exclusive : ErrorKind
/**
* M_FORBIDDEN
*
* Forbidden access, e.g. joining a room without permission, failed login.
*/
data object Forbidden : ErrorKind
/**
* M_GUEST_ACCESS_FORBIDDEN
*
* The room or resource does not permit guests to access it.
*
* guests: https://spec.matrix.org/latest/client-server-api/#guest-access
*/
data object GuestAccessForbidden : ErrorKind
/**
* M_INCOMPATIBLE_ROOM_VERSION
*
* The client attempted to join a room that has a version the server does
* not support.
*/
data class IncompatibleRoomVersion(
/**
* The room's version.
*/
val roomVersion: String
) : ErrorKind
/**
* M_INVALID_PARAM
*
* A parameter that was specified has the wrong value. For example, the
* server expected an integer and instead received a string.
*/
data object InvalidParam : ErrorKind
/**
* M_INVALID_ROOM_STATE
*
* The initial state implied by the parameters to the create_room
* request is invalid, e.g. the user's power_level is set below that
* necessary to set the room name.
*
*/
data object InvalidRoomState : ErrorKind
/**
* M_INVALID_USERNAME
*
* The desired user name is not valid.
*/
data object InvalidUsername : ErrorKind
/**
* M_LIMIT_EXCEEDED
*
* The request has been refused due to rate limiting: too many requests
* have been sent in a short period of time.
*
* rate limiting: https://spec.matrix.org/latest/client-server-api/#rate-limiting
*/
data class LimitExceeded(
/**
* How long a client should wait before they can try again.
*/
val retryAfterMs: Long?
) : ErrorKind
/**
* M_MISSING_PARAM
*
* A required parameter was missing from the request.
*/
data object MissingParam : ErrorKind
/**
* M_MISSING_TOKEN
*
* No access token was specified for the request, but one is required.
*
* access token: https://spec.matrix.org/latest/client-server-api/#client-authentication
*/
data object MissingToken : ErrorKind
/**
* M_NOT_FOUND
*
* No resource was found for this request.
*/
data object NotFound : ErrorKind
/**
* M_NOT_JSON
*
* The request did not contain valid JSON.
*/
data object NotJson : ErrorKind
/**
* M_NOT_YET_UPLOADED
*
* An mxc URI generated was used and the content is not yet available.
*
*/
data object NotYetUploaded : ErrorKind
/**
* M_RESOURCE_LIMIT_EXCEEDED
*
* The request cannot be completed because the homeserver has reached a
* resource limit imposed on it. For example, a homeserver held in a
* shared hosting environment may reach a resource limit if it starts
* using too much memory or disk space.
*/
data class ResourceLimitExceeded(
/**
* A URI giving a contact method for the server administrator.
*/
val adminContact: String
) : ErrorKind
/**
* M_ROOM_IN_USE
*
* The room alias specified in the request is already taken.
*
* room alias: https://spec.matrix.org/latest/client-server-api/#room-aliases
*/
data object RoomInUse : ErrorKind
/**
* M_SERVER_NOT_TRUSTED
*
* The client's request used a third-party server, e.g. identity server,
* that this server does not trust.
*/
data object ServerNotTrusted : ErrorKind
/**
* M_THREEPID_AUTH_FAILED
*
* Authentication could not be performed on the third-party identifier.
*
* third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
*/
data object ThreepidAuthFailed : ErrorKind
/**
* M_THREEPID_DENIED
*
* The server does not permit this third-party identifier. This may
* happen if the server only permits, for example, email addresses from
* a particular domain.
*
* third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
*/
data object ThreepidDenied : ErrorKind
/**
* M_THREEPID_IN_USE
*
* The third-party identifier is already in use by another user.
*
* third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
*/
data object ThreepidInUse : ErrorKind
/**
* M_THREEPID_MEDIUM_NOT_SUPPORTED
*
* The homeserver does not support adding a third-party identifier of the
* given medium.
*
* third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
*/
data object ThreepidMediumNotSupported : ErrorKind
/**
* M_THREEPID_NOT_FOUND
*
* No account matching the given third-party identifier could be found.
*
* third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
*/
data object ThreepidNotFound : ErrorKind
/**
* M_TOO_LARGE
*
* The request or entity was too large.
*/
data object TooLarge : ErrorKind
/**
* M_UNABLE_TO_AUTHORISE_JOIN
*
* The room is restricted and none of the conditions can be validated by
* the homeserver. This can happen if the homeserver does not know
* about any of the rooms listed as conditions, for example.
*
* restricted: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
*/
data object UnableToAuthorizeJoin : ErrorKind
/**
* M_UNABLE_TO_GRANT_JOIN
*
* A different server should be attempted for the join. This is typically
* because the resident server can see that the joining user satisfies
* one or more conditions, such as in the case of restricted rooms,
* but the resident server would be unable to meet the authorization
* rules.
*
* restricted rooms: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
*/
data object UnableToGrantJoin : ErrorKind
/**
* M_UNAUTHORIZED
*
* The request was not correctly authorized. Usually due to login failures.
*/
data object Unauthorized : ErrorKind
/**
* M_UNKNOWN
*
* An unknown error has occurred.
*/
data object Unknown : ErrorKind
/**
* M_UNKNOWN_TOKEN
*
* The access or refresh token specified was not recognized.
*
* access or refresh token: https://spec.matrix.org/latest/client-server-api/#client-authentication
*/
data class UnknownToken(
/**
* If this is true, the client is in a "soft logout" state, i.e.
* the server requires re-authentication but the session is not
* invalidated. The client can acquire a new access token by
* specifying the device ID it is already using to the login API.
*
* soft logout: https://spec.matrix.org/latest/client-server-api/#soft-logout
*/
val softLogout: Boolean
) : ErrorKind
/**
* M_UNRECOGNIZED
*
* The server did not understand the request.
*
* This is expected to be returned with a 404 HTTP status code if the
* endpoint is not implemented or a 405 HTTP status code if the
* endpoint is implemented, but the incorrect HTTP method is used.
*/
data object Unrecognized : ErrorKind
/**
* M_UNSUPPORTED_ROOM_VERSION
*
* The request to create_room used a room version that the server does
* not support.
*
*/
data object UnsupportedRoomVersion : ErrorKind
/**
* M_URL_NOT_SET
*
* The application service doesn't have a URL configured.
*/
data object UrlNotSet : ErrorKind
/**
* M_USER_DEACTIVATED
*
* The user ID associated with the request has been deactivated.
*/
data object UserDeactivated : ErrorKind
/**
* M_USER_IN_USE
*
* The desired user ID is already taken.
*/
data object UserInUse : ErrorKind
/**
* M_USER_LOCKED
*
* The account has been locked and cannot be used at this time.
*
* locked: https://spec.matrix.org/latest/client-server-api/#account-locking
*/
data object UserLocked : ErrorKind
/**
* M_USER_SUSPENDED
*
* The account has been suspended and can only be used for limited
* actions at this time.
*
* suspended: https://spec.matrix.org/latest/client-server-api/#account-suspension
*/
data object UserSuspended : ErrorKind
/**
* M_WEAK_PASSWORD
*
* The password was rejected by the server for being too weak.
*
* rejected: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
*/
data object WeakPassword : ErrorKind
/**
* M_WRONG_ROOM_KEYS_VERSION
*
* The version of the room keys backup provided in the request does not
* match the current backup version.
*
* room keys backup: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
*/
data class WrongRoomKeysVersion(
/**
* The currently active backup version.
*/
val currentVersion: String?
) : ErrorKind
/**
* A custom API error.
*/
data class Custom(val errcode: String) : ErrorKind
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.exception
/**
* Exceptions that can occur while resolving the events associated to push notifications.
*/
sealed class NotificationResolverException : Exception() {
/**
* The event was not found by the notification service.
*/
data object EventNotFound : NotificationResolverException()
/**
* The event was found but it was filtered out by the notification service.
*/
data object EventFilteredOut : NotificationResolverException()
/**
* An unexpected error occurred while trying to resolve the event.
*/
data class UnknownError(override val message: String) : NotificationResolverException()
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration
data class AudioDetails(
val duration: Duration,
val waveform: ImmutableList<Float>,
)
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import kotlin.time.Duration
data class AudioInfo(
val duration: Duration?,
val size: Long?,
val mimetype: String?,
)
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
data class FileInfo(
val mimetype: String?,
val size: Long?,
val thumbnailInfo: ThumbnailInfo?,
val thumbnailSource: MediaSource?
)
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
data class ImageInfo(
val height: Long?,
val width: Long?,
val mimetype: String?,
val size: Long?,
val thumbnailInfo: ThumbnailInfo?,
val thumbnailSource: MediaSource?,
val blurhash: String?
)
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
interface MatrixMediaLoader {
/**
* @param source to fetch the content for.
* @return a [Result] of ByteArray. It contains the binary data for the media.
*/
suspend fun loadMediaContent(source: MediaSource): Result<ByteArray>
/**
* @param source to fetch the data for.
* @param width: the desired width for rescaling the media as thumbnail
* @param height: the desired height for rescaling the media as thumbnail
* @return a [Result] of ByteArray. It contains the binary data for the media.
*/
suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result<ByteArray>
/**
* @param source to fetch the data for.
* @param mimeType: optional mime type.
* @param filename: optional String which will be used to name the file.
* @param useCache: if true, the rust sdk will cache the media in its store.
* @return a [Result] of [MediaFile]
*/
suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
filename: String?,
useCache: Boolean = true,
): Result<MediaFile>
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import java.io.Closeable
import java.io.File
/**
* A wrapper around a media file on the disk.
* When closed the file will be removed from the disk unless [persist] has been used.
*/
interface MediaFile : Closeable {
fun path(): String
/**
* Persists the temp file to the given path. The file will be moved to
* the given path and won't be deleted anymore when closing the handle.
*/
fun persist(path: String): Boolean
}
fun MediaFile.toFile(): File {
return File(path())
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
/**
* Configuration for media preview ie. invite avatars and timeline media.
*/
data class MediaPreviewConfig(
val mediaPreviewValue: MediaPreviewValue,
val hideInviteAvatar: Boolean,
) {
companion object {
/**
* The default config if unknown (no local nor server config).
*/
val DEFAULT = MediaPreviewConfig(
mediaPreviewValue = MediaPreviewValue.DEFAULT,
hideInviteAvatar = false
)
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import kotlinx.coroutines.flow.StateFlow
interface MediaPreviewService {
/**
* Will fetch the media preview config from the server.
*/
suspend fun fetchMediaPreviewConfig(): Result<MediaPreviewConfig?>
/**
* Will emit the media preview config known by the client.
* This will emit a new value when received from sync.
*/
val mediaPreviewConfigFlow: StateFlow<MediaPreviewConfig>
/**
* Set the media preview display policy. This will update the value on the server and update the local value when successful.
*/
suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result<Unit>
/**
* Set the invite avatars display policy. This will update the value on the server and update the local value when successful.
*/
suspend fun setHideInviteAvatars(hide: Boolean): Result<Unit>
}
fun MediaPreviewService.getMediaPreviewValue() = mediaPreviewConfigFlow.value.mediaPreviewValue
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Off
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.On
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Private
import io.element.android.libraries.matrix.api.room.join.JoinRule
/**
* Represents the values for media preview settings.
* - [On] means that media preview are enabled
* - [Off] means that media preview are disabled
* - [Private] means that media preview are enabled only for private chats.
*/
enum class MediaPreviewValue {
On,
Off,
Private;
companion object {
/**
* The default value if unknown (no local nor server config).
*/
val DEFAULT = On
}
}
fun MediaPreviewValue?.isPreviewEnabled(joinRule: JoinRule?): Boolean {
return when (this) {
null, On -> true
Off -> false
Private -> when (joinRule) {
is JoinRule.Private,
is JoinRule.Knock,
is JoinRule.Invite,
is JoinRule.Restricted,
is JoinRule.KnockRestricted -> true
else -> false
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MediaSource(
/**
* Url of the media.
*/
val url: String,
/**
* This is used to hold data for encrypted media.
*/
val json: String? = null,
) : Parcelable
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
/**
* This is an abstraction over the Rust SDK's `SendAttachmentJoinHandle` which allows us to either [await] the upload process or [cancel] it.
*/
interface MediaUploadHandler {
/** Await the upload process to finish. */
suspend fun await(): Result<Unit>
/** Cancel the upload process. */
fun cancel()
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
data class ThumbnailInfo(
val height: Long?,
val width: Long?,
val mimetype: String?,
val size: Long?
)
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import kotlin.time.Duration
data class VideoInfo(
val duration: Duration?,
val height: Long?,
val width: Long?,
val mimetype: String?,
val size: Long?,
val thumbnailInfo: ThumbnailInfo?,
val thumbnailSource: MediaSource?,
val blurhash: String?
)
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.mxc
interface MxcTools {
/**
* Sanitizes an mxcUri to be used as a relative file path.
*
* @param mxcUri the Matrix Content (mxc://) URI of the file.
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
*/
fun mxcUri2FilePath(mxcUri: String): String?
}
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
data class NotificationData(
val sessionId: SessionId,
val eventId: EventId,
val threadId: ThreadId?,
val roomId: RoomId,
// mxc url
val senderAvatarUrl: String?,
// private, must use `getDisambiguatedDisplayName`
private val senderDisplayName: String?,
private val senderIsNameAmbiguous: Boolean,
val roomAvatarUrl: String?,
val roomDisplayName: String?,
val isDirect: Boolean,
val isDm: Boolean,
val isEncrypted: Boolean,
val isNoisy: Boolean,
val timestamp: Long,
val content: NotificationContent,
val hasMention: Boolean,
) {
fun getDisambiguatedDisplayName(userId: UserId): String = when {
senderDisplayName.isNullOrBlank() -> userId.value
senderIsNameAmbiguous -> "$senderDisplayName ($userId)"
else -> senderDisplayName
}
}
sealed interface NotificationContent {
sealed interface MessageLike : NotificationContent {
data object CallAnswer : MessageLike
data class CallInvite(
val senderId: UserId,
) : MessageLike
data class RtcNotification(
val senderId: UserId,
val type: RtcNotificationType,
val expirationTimestampMillis: Long
) : MessageLike
data object CallHangup : MessageLike
data object CallCandidates : MessageLike
data object KeyVerificationReady : MessageLike
data object KeyVerificationStart : MessageLike
data object KeyVerificationCancel : MessageLike
data object KeyVerificationAccept : MessageLike
data object KeyVerificationKey : MessageLike
data object KeyVerificationMac : MessageLike
data object KeyVerificationDone : MessageLike
data class ReactionContent(
val relatedEventId: String
) : MessageLike
data object RoomEncrypted : MessageLike
data class RoomMessage(
val senderId: UserId,
val messageType: MessageType
) : MessageLike
data class RoomRedaction(
val redactedEventId: EventId?,
val reason: String?,
) : MessageLike
data object Sticker : MessageLike
data class Poll(
val senderId: UserId,
val question: String,
) : MessageLike
}
sealed interface StateEvent : NotificationContent {
data object PolicyRuleRoom : StateEvent
data object PolicyRuleServer : StateEvent
data object PolicyRuleUser : StateEvent
data object RoomAliases : StateEvent
data object RoomAvatar : StateEvent
data object RoomCanonicalAlias : StateEvent
data object RoomCreate : StateEvent
data object RoomEncryption : StateEvent
data object RoomGuestAccess : StateEvent
data object RoomHistoryVisibility : StateEvent
data object RoomJoinRules : StateEvent
data class RoomMemberContent(
val userId: UserId,
val membershipState: RoomMembershipState
) : StateEvent
data object RoomName : StateEvent
data object RoomPinnedEvents : StateEvent
data object RoomPowerLevels : StateEvent
data object RoomServerAcl : StateEvent
data object RoomThirdPartyInvite : StateEvent
data object RoomTombstone : StateEvent
data class RoomTopic(val topic: String) : StateEvent
data object SpaceChild : StateEvent
data object SpaceParent : StateEvent
}
data class Invite(
val senderId: UserId,
) : NotificationContent
}
enum class RtcNotificationType {
RING,
NOTIFY
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Represents the resolution state of an attempt to retrieve notification data for a set of event ids.
* The outer [Result] indicates the success or failure of the setup to retrieve notifications.
* The inner [Result] for each [EventId] in the map indicates whether the notification data was successfully retrieved or if there was an error.
*/
typealias GetNotificationDataResult = Result<Map<EventId, Result<NotificationData>>>
/**
* Service to retrieve notifications for a given set of event ids in specific rooms.
*/
interface NotificationService {
/**
* Fetch notifications for the specified event ids in the given rooms.
*/
suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): GetNotificationDataResult
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.notificationsettings
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
import kotlinx.coroutines.flow.SharedFlow
interface NotificationSettingsService {
/**
* State of the current room notification settings flow ([RoomNotificationSettingsState.Unknown] if not started).
*/
val notificationSettingsChangeFlow: SharedFlow<Unit>
suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings>
suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode>
suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result<Unit>
suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit>
suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit>
suspend fun muteRoom(roomId: RoomId): Result<Unit>
suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<Unit>
suspend fun isRoomMentionEnabled(): Result<Boolean>
suspend fun setRoomMentionEnabled(enabled: Boolean): Result<Unit>
suspend fun isCallEnabled(): Result<Boolean>
suspend fun setCallEnabled(enabled: Boolean): Result<Unit>
suspend fun isInviteForMeEnabled(): Result<Boolean>
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
suspend fun getRoomsWithUserDefinedRules(): Result<List<RoomId>>
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
suspend fun getRawPushRules(): Result<String?>
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.oidc
import io.element.android.libraries.matrix.api.core.DeviceId
sealed interface AccountManagementAction {
data object Profile : AccountManagementAction
data object SessionsList : AccountManagementAction
data class SessionView(val deviceId: DeviceId) : AccountManagementAction
data class SessionEnd(val deviceId: DeviceId) : AccountManagementAction
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
/**
* Mapping of an input URI to a matrix.to compliant URI.
*/
interface MatrixToConverter {
/**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url.
* Examples:
* - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
*/
fun convert(uri: Uri): Uri?
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.permalink
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.UserId
interface PermalinkBuilder {
fun permalinkForUser(userId: UserId): Result<String>
fun permalinkForRoomAlias(roomAlias: RoomAlias): Result<String>
}
sealed class PermalinkBuilderError : Throwable() {
data object InvalidData : PermalinkBuilderError()
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.parcelize.Parcelize
/**
* This sealed class represents all the permalink cases.
* You don't have to instantiate yourself but should use [PermalinkParser] instead.
*/
@Immutable
@Parcelize
sealed interface PermalinkData : Parcelable {
data class RoomLink(
val roomIdOrAlias: RoomIdOrAlias,
val eventId: EventId? = null,
val threadId: ThreadId? = null,
val viaParameters: ImmutableList<String> = persistentListOf()
) : PermalinkData
/*
* &room_name=Team2
* &room_avatar_url=mxc:
* &inviter_name=bob
*/
data class RoomEmailInviteLink(
val roomId: RoomId,
val email: String,
val signUrl: String,
val roomName: String?,
val roomAvatarUrl: String?,
val inviterName: String?,
val identityServer: String,
val token: String,
val privateKey: String,
val roomType: String?
) : PermalinkData
data class UserLink(val userId: UserId) : PermalinkData
data class FallbackLink(val uri: Uri, val isLegacyGroupLink: Boolean = false) : PermalinkData
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.permalink
/**
* This class turns a uri to a [PermalinkData].
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org)
* or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org)
*/
interface PermalinkParser {
/**
* Turns a uri string to a [PermalinkData].
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
fun parse(uriString: String): PermalinkData
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.platform
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
/**
* This service is responsible for initializing the platform-related settings of the SDK.
*/
interface InitPlatformService {
/**
* Initialize the platform-related settings of the SDK.
* @param tracingConfiguration the tracing configuration to use for logging.
*/
fun init(tracingConfiguration: TracingConfiguration)
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.poll
data class PollAnswer(
val id: String,
val text: String
)
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.poll
enum class PollKind {
/** Voters should see results as soon as they have voted. */
Disclosed,
/** Results should be only revealed when the poll is ended. */
Undisclosed,
}
val PollKind.isDisclosed: Boolean
get() = this == PollKind.Disclosed
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.pusher
interface PushersService {
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit>
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.pusher
data class SetHttpPusherData(
val pushKey: String,
val appId: String,
val url: String,
val appDisplayName: String,
val deviceDisplayName: String,
val profileTag: String?,
val lang: String,
val defaultPayload: String,
)
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.pusher
data class UnsetHttpPusherData(
val pushKey: String,
val appId: String,
)
@@ -0,0 +1,261 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
/**
* This interface represents the common functionality for a local room, whether it's joined, invited, knocked, or left.
*/
interface BaseRoom : Closeable {
/**
* The session id of the current user.
*/
val sessionId: SessionId
/**
* The id of the room.
*/
val roomId: RoomId
/**
* The coroutine scope that will handle all jobs related to this room.
*/
val roomCoroutineScope: CoroutineScope
/**
* The current loaded members as a StateFlow.
* Initial value is [RoomMembersState.Unknown].
* To update them you should call [updateMembers].
*/
val membersStateFlow: StateFlow<RoomMembersState>
/**
* A flow that emits the current [RoomInfo] state.
*/
val roomInfoFlow: StateFlow<RoomInfo>
/**
* Get the latest room info we have received from the SDK stream.
*/
fun info(): RoomInfo = roomInfoFlow.value
fun predecessorRoom(): PredecessorRoom?
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/**
* Try to load the room members and update the membersFlow.
*/
suspend fun updateMembers()
/**
* Get the members of the room. Note: generally this should not be used, please use
* [membersStateFlow] and [updateMembers] instead.
*/
suspend fun getMembers(limit: Int = 5): Result<List<RoomMember>>
/**
* Will return an updated member or an error.
*/
suspend fun getUpdatedMember(userId: UserId): Result<RoomMember>
/**
* Adds the room to the sync subscription list.
*/
suspend fun subscribeToSync()
/**
* Gets the power levels of the room.
*/
suspend fun powerLevels(): Result<RoomPowerLevelsValues>
/**
* Gets the role of the user with the provided [userId] in the room.
*/
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
/**
* Gets the display name of the user with the provided [userId] in the room.
*/
suspend fun userDisplayName(userId: UserId): Result<String?>
/**
* Gets the avatar of the user with the provided [userId] in the room.
*/
suspend fun userAvatarUrl(userId: UserId): Result<String?>
/**
* Leaves and forgets the room. Only joined, invited or knocked rooms can be left.
*/
suspend fun leave(): Result<Unit>
/**
* Joins the room. Only invited rooms can be joined.
*/
suspend fun join(): Result<Unit>
/**
* Forgets about the room, removing it from the server and the local cache. Only left and banned rooms can be forgotten.
*/
suspend fun forget(): Result<Unit>
/**
* Returns `true` if the user with the provided [userId] can invite other users to the room.
*/
suspend fun canUserInvite(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can kick other users from the room.
*/
suspend fun canUserKick(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can ban other users from the room.
*/
suspend fun canUserBan(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can redact their own messages.
*/
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can redact messages from other users.
*/
suspend fun canUserRedactOther(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can send state events.
*/
suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can send messages.
*/
suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can trigger an `@room` notification.
*/
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can pin or unpin messages.
*/
suspend fun canUserPinUnpin(userId: UserId): Result<Boolean>
/**
* Returns `true` if the user with the provided [userId] can join or starts calls.
*/
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
canUserSendState(userId, StateEventType.CALL_MEMBER)
/**
* Sets the room as favorite or not, based on the [isFavorite] parameter.
*/
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
/**
* Mark the room as read by trying to attach an unthreaded read receipt to the latest room event.
*
* Note this will instantiate a new timeline, which is an expensive operation.
* Prefer using [Timeline.markAsRead] instead when possible.
*
* @param receiptType The type of receipt to send.
*/
suspend fun markAsRead(receiptType: ReceiptType): Result<Unit>
/**
* Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag.
* @param isUnread true to mark the room as unread, false to remove the flag.
*/
suspend fun setUnreadFlag(isUnread: Boolean): Result<Unit>
/**
* Clear the event cache storage for the current room.
*/
suspend fun clearEventCacheStorage(): Result<Unit>
/**
* Get the permalink for the room.
*/
suspend fun getPermalink(): Result<String>
/**
* Get the permalink for the provided [eventId].
* @param eventId The event id to get the permalink for.
* @return The permalink, or a failure.
*/
suspend fun getPermalinkFor(eventId: EventId): Result<String>
/**
* Returns the visibility for this room in the room directory.
* If the room is not published, the result will be [RoomVisibility.Private].
*/
suspend fun getRoomVisibility(): Result<RoomVisibility>
/**
* Returns the visibility for this room in the room directory, fetching it from the homeserver if needed.
*/
suspend fun getUpdatedIsEncrypted(): Result<Boolean>
/**
* Store the given `ComposerDraft` in the state store of this room.
*/
suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result<Unit>
/**
* Retrieve the `ComposerDraft` stored in the state store for this room.
*/
suspend fun loadComposerDraft(threadRoot: ThreadId?): Result<ComposerDraft?>
/**
* Clear the `ComposerDraft` stored in the state store for this room.
*/
suspend fun clearComposerDraft(threadRoot: ThreadId?): Result<Unit>
/**
* Reports a room as inappropriate to the server.
* The caller is not required to be joined to the room to report it.
* @param reason - The reason the room is being reported.
*/
suspend fun reportRoom(reason: String?): Result<Unit>
suspend fun declineCall(notificationEventId: EventId): Result<Unit>
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId>
suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?>
/**
* Destroy the room and release all resources associated to it.
*/
fun destroy()
override fun close() = destroy()
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ThreadId
sealed interface CreateTimelineParams {
data class Focused(val focusedEventId: EventId) : CreateTimelineParams
data object MediaOnly : CreateTimelineParams
data class MediaOnlyFocused(val focusedEventId: EventId) : CreateTimelineParams
data object PinnedOnly : CreateTimelineParams
data class Threaded(val threadRootEventId: ThreadId) : CreateTimelineParams
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
enum class CurrentUserMembership {
INVITED,
JOINED,
LEFT,
KNOCKED,
BANNED,
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.core.bool.orFalse
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
/**
* Method to filter members by userId or displayName.
* It does filter through the already known members, it doesn't perform additional requests.
*/
suspend fun BaseRoom.filterMembers(query: String, coroutineContext: CoroutineContext): List<RoomMember> = withContext(coroutineContext) {
val roomMembersState = membersStateFlow.value
val activeRoomMembers = roomMembersState.roomMembers()
?.filter { it.membership.isActive() }
.orEmpty()
val filteredMembers = if (query.isBlank()) {
activeRoomMembers
} else {
activeRoomMembers.filter { member ->
member.userId.value.contains(query, ignoreCase = true) ||
member.displayName?.contains(query, ignoreCase = true).orFalse()
}
}
filteredMembers
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.RoomId
class ForwardEventException(
val roomIds: List<RoomId>
) : Exception() {
override val message: String? = "Failed to deliver event to $roomIds rooms"
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.UserId
sealed interface IntentionalMention {
data class User(val userId: UserId) : IntentionalMention
data object Room : IntentionalMention
}
@@ -0,0 +1,180 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface JoinedRoom : BaseRoom {
val syncUpdateFlow: StateFlow<Long>
val roomTypingMembersFlow: Flow<List<UserId>>
val identityStateChangesFlow: Flow<List<IdentityStateChange>>
val roomNotificationSettingsStateFlow: StateFlow<RoomNotificationSettingsState>
/**
* The current knock requests in the room as a Flow.
*/
val knockRequestsFlow: Flow<List<KnockRequest>>
/**
* The live timeline of the room. Must be used to send Event to a room.
*/
val liveTimeline: Timeline
/**
* Create a new timeline.
* @param createTimelineParams contains parameters about how to filter the timeline. Will also configure the date separators.
*/
suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline>
suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
/**
* Send a typing notification.
* @param isTyping True if the user is typing, false otherwise.
*/
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
suspend fun inviteUserById(id: UserId): Result<Unit>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit>
suspend fun updateRoomNotificationSettings(): Result<Unit>
/**
* Update the canonical alias of the room.
*
* Note that publishing the alias in the room directory is done separately.
*/
suspend fun updateCanonicalAlias(
canonicalAlias: RoomAlias?,
alternativeAliases: List<RoomAlias>
): Result<Unit>
/**
* Update the room's visibility in the room directory.
*/
suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result<Unit>
/**
* Update room history visibility for this room.
*/
suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result<Unit>
/**
* Publish a new room alias for this room in the room directory.
*
* Returns:
* - `true` if the room alias didn't exist and it's now published.
* - `false` if the room alias was already present so it couldn't be
* published.
*/
suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result<Boolean>
/**
* Remove an existing room alias for this room in the room directory.
*
* Returns:
* - `true` if the room alias was present and it's now removed from the
* room directory.
* - `false` if the room alias didn't exist so it couldn't be removed.
*/
suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result<Boolean>
/**
* Enable End-to-end encryption in this room.
*/
suspend fun enableEncryption(): Result<Unit>
/**
* Update the join rule for this room.
*/
suspend fun updateJoinRule(joinRule: JoinRule): Result<Unit>
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
suspend fun updatePowerLevels(roomPowerLevelsValues: RoomPowerLevelsValues): Result<Unit>
suspend fun resetPowerLevels(): Result<Unit>
suspend fun setName(name: String): Result<Unit>
suspend fun setTopic(topic: String): Result<Unit>
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
suspend fun kickUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun banUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun unbanUser(userId: UserId, reason: String? = null): Result<Unit>
/**
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
* @param widgetSettings The widget settings to use.
* @param clientId The client id to use. It should be unique per app install.
* @param languageTag The language tag to use. If null, the default language will be used.
* @param theme The theme to use. If null, the default theme will be used.
* @return The resulting url, or a failure.
*/
suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
languageTag: String?,
theme: String?,
): Result<String>
/**
* Get a [MatrixWidgetDriver] for the provided [widgetSettings].
* @param widgetSettings The widget settings to use.
* @return The resulting [MatrixWidgetDriver], or a failure.
*/
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
suspend fun setSendQueueEnabled(enabled: Boolean)
/**
* Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified.
*
* @param devices The map of users identifiers to device identifiers received in the error
* @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload.
*
*/
suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, sendHandle: SendHandle): Result<Unit>
/**
* Remove verification requirements for the given users and
* resend messages that failed to send because their identities were no longer verified.
*
* @param userIds The list of users identifiers received in the error.
* @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload.
*
*/
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit>
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
@Immutable
sealed interface MessageEventType {
data object CallAnswer : MessageEventType
data object CallInvite : MessageEventType
data object CallHangup : MessageEventType
data object CallCandidates : MessageEventType
data object RtcNotification : MessageEventType
data object KeyVerificationReady : MessageEventType
data object KeyVerificationStart : MessageEventType
data object KeyVerificationCancel : MessageEventType
data object KeyVerificationAccept : MessageEventType
data object KeyVerificationKey : MessageEventType
data object KeyVerificationMac : MessageEventType
data object KeyVerificationDone : MessageEventType
data object Reaction : MessageEventType
data object RoomEncrypted : MessageEventType
data object RoomMessage : MessageEventType
data object RoomRedaction : MessageEventType
data object Sticker : MessageEventType
data object PollEnd : MessageEventType
data object PollResponse : MessageEventType
data object PollStart : MessageEventType
data object UnstablePollEnd : MessageEventType
data object UnstablePollResponse : MessageEventType
data object UnstablePollStart : MessageEventType
data class Other(val type: String) : MessageEventType
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
/** A reference to a room either invited, knocked or banned. */
interface NotJoinedRoom : AutoCloseable {
val previewInfo: RoomPreviewInfo
val localRoom: BaseRoom?
/**
* Get the membership details of the user in the room, as well as from the user who sent the `m.room.member` event.
*/
suspend fun membershipDetails(): Result<RoomMembershipDetails?>
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class RoomInfo(
val id: RoomId,
/** The room's name from the room state event if received from sync, or one that's been computed otherwise. */
val name: String?,
/** Room name as defined by the room state event only. */
val rawName: String?,
val topic: String?,
val avatarUrl: String?,
val isPublic: Boolean?,
val isDirect: Boolean,
val isEncrypted: Boolean?,
val joinRule: JoinRule?,
val isSpace: Boolean,
val isFavorite: Boolean,
val canonicalAlias: RoomAlias?,
val alternativeAliases: ImmutableList<RoomAlias>,
val currentUserMembership: CurrentUserMembership,
/**
* Member who invited the current user to a room that's in the invited
* state.
*
* Can be missing if the room membership invite event is missing from the
* store.
*/
val inviter: RoomMember?,
val activeMembersCount: Long,
val invitedMembersCount: Long,
val joinedMembersCount: Long,
val roomPowerLevels: RoomPowerLevels?,
val highlightCount: Long,
val notificationCount: Long,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val activeRoomCallParticipants: ImmutableList<UserId>,
val isMarkedUnread: Boolean,
/**
* "Interesting" messages received in that room, independently of the
* notification settings.
*/
val numUnreadMessages: Long,
/**
* Events that will notify the user, according to their
* notification settings.
*/
val numUnreadNotifications: Long,
/**
* Events causing mentions/highlights for the user, according to their
* notification settings.
*/
val numUnreadMentions: Long,
val heroes: ImmutableList<MatrixUser>,
val pinnedEventIds: ImmutableList<EventId>,
val creators: ImmutableList<UserId>,
val historyVisibility: RoomHistoryVisibility,
val successorRoom: SuccessorRoom?,
val roomVersion: String?,
val privilegedCreatorRole: Boolean,
) {
val aliases: List<RoomAlias>
get() = listOfNotNull(canonicalAlias) + alternativeAliases
/**
* Returns the list of users with the given [role] in this room.
*/
fun usersWithRole(role: RoomMember.Role): List<UserId> {
return if (role is RoomMember.Role.Owner && role.isCreator) {
this.creators
} else {
this.roomPowerLevels?.usersWithRole(role).orEmpty().toList()
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import kotlinx.coroutines.flow.first
/**
* Returns whether the room with the provided info is a DM.
* A DM is a room with at most 2 active members (one of them may have left).
*
* @param isDirect true if the room is direct
* @param activeMembersCount the number of active members in the room (joined or invited)
*/
fun isDm(isDirect: Boolean, activeMembersCount: Int): Boolean {
return isDirect && activeMembersCount <= 2
}
/**
* Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo].
*/
suspend fun BaseRoom.isDm() = roomInfoFlow.first().isDm
/**
* Returns whether the [RoomInfo] is from a DM.
*/
val RoomInfo.isDm get() = isDm(isDirect, activeMembersCount.toInt())
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
data class RoomMember(
val userId: UserId,
val displayName: String?,
val avatarUrl: String?,
val membership: RoomMembershipState,
val isNameAmbiguous: Boolean,
val powerLevel: Long,
val isIgnored: Boolean,
val role: Role,
val membershipChangeReason: String?,
) {
/**
* Role of the RoomMember, based on its [powerLevel].
*/
@Immutable
sealed interface Role {
data class Owner(val isCreator: Boolean) : Role
data object Admin : Role
data object Moderator : Role
data object User : Role
val powerLevel: Long
get() = when (this) {
is Owner -> if (isCreator) CREATOR_POWERLEVEL else SUPERADMIN_POWERLEVEL
Admin -> ADMIN_POWERLEVEL
Moderator -> MODERATOR_POWERLEVEL
User -> USER_POWERLEVEL
}
companion object {
private const val CREATOR_POWERLEVEL = Long.MAX_VALUE
private const val SUPERADMIN_POWERLEVEL = 150L
private const val ADMIN_POWERLEVEL = 100L
private const val MODERATOR_POWERLEVEL = 50L
private const val USER_POWERLEVEL = 0L
fun forPowerLevel(powerLevel: Long): Role {
return when {
powerLevel == CREATOR_POWERLEVEL -> Owner(isCreator = true)
powerLevel >= SUPERADMIN_POWERLEVEL -> Owner(isCreator = false)
powerLevel >= ADMIN_POWERLEVEL -> Admin
powerLevel >= MODERATOR_POWERLEVEL -> Moderator
else -> User
}
}
}
}
/**
* Disambiguated display name for the RoomMember.
* If the display name is null, the user ID is returned.
* If the display name is ambiguous, the user ID is appended in parentheses.
* Otherwise, the display name is returned.
*/
val disambiguatedDisplayName: String = when {
displayName == null -> userId.value
isNameAmbiguous -> "$displayName ($userId)"
else -> displayName
}
val displayNameOrDefault: String
get() = when {
displayName == null -> userId.extractedDisplayName
else -> displayName
}
}
enum class RoomMembershipState {
BAN,
INVITE,
JOIN,
KNOCK,
LEAVE;
fun isActive(): Boolean = this == JOIN || this == INVITE
}
/**
* Returns the best name value to display for the RoomMember.
* If the [RoomMember.displayName] is present and not empty it'll be used, otherwise the [RoomMember.userId] will be used.
*/
fun RoomMember.getBestName(): String {
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
}
fun RoomMember.toMatrixUser() = MatrixUser(
userId = userId,
displayName = displayName,
avatarUrl = avatarUrl,
)
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.collections.immutable.ImmutableList
@Immutable
sealed interface RoomMembersState {
data object Unknown : RoomMembersState
data class Pending(val prevRoomMembers: ImmutableList<RoomMember>? = null) : RoomMembersState
data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList<RoomMember>? = null) : RoomMembersState
data class Ready(val roomMembers: ImmutableList<RoomMember>) : RoomMembersState
}
fun RoomMembersState.roomMembers(): List<RoomMember>? {
return when (this) {
is RoomMembersState.Ready -> roomMembers
is RoomMembersState.Pending -> prevRoomMembers
is RoomMembersState.Error -> prevRoomMembers
else -> null
}
}
fun RoomMembersState.joinedRoomMembers(): List<RoomMember> {
return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN }
}
fun RoomMembersState.activeRoomMembers(): List<RoomMember> {
return roomMembers().orEmpty().filter { it.membership.isActive() }
}
fun RoomMembersState.getDirectRoomMember(roomInfo: RoomInfo, sessionId: SessionId): RoomMember? {
return roomMembers()
?.takeIf { roomInfo.isDm }
?.find { it.userId != sessionId && it.membership.isActive() }
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
/**
* Room membership details for the current user and the sender of the membership event.
*
* It also includes the reason the current user's membership changed, if any.
*/
data class RoomMembershipDetails(
val currentUserMember: RoomMember,
val senderMember: RoomMember?,
) {
val membershipChangeReason: String? = currentUserMember.membershipChangeReason
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class RoomMembershipObserver {
data class RoomMembershipUpdate(
val roomId: RoomId,
val isSpace: Boolean,
val isUserInRoom: Boolean,
val change: MembershipChange,
)
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10)
val updates = _updates.asSharedFlow()
suspend fun notifyUserLeftRoom(
roomId: RoomId,
isSpace: Boolean,
membershipBeforeLeft: CurrentUserMembership,
) {
val membershipChange = when (membershipBeforeLeft) {
CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED
CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED
else -> MembershipChange.LEFT
}
_updates.emit(
RoomMembershipUpdate(
roomId = roomId,
isSpace = isSpace,
isUserInRoom = false,
change = membershipChange,
)
)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
data class RoomNotificationSettings(
val mode: RoomNotificationMode,
val isDefault: Boolean,
)
enum class RoomNotificationMode {
ALL_MESSAGES,
MENTIONS_AND_KEYWORDS_ONLY,
MUTE
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
sealed interface RoomNotificationSettingsState {
data object Unknown : RoomNotificationSettingsState
data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : RoomNotificationSettingsState
data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : RoomNotificationSettingsState
data class Ready(val roomNotificationSettings: RoomNotificationSettings) : RoomNotificationSettingsState
}
fun RoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? {
return when (this) {
is RoomNotificationSettingsState.Ready -> roomNotificationSettings
is RoomNotificationSettingsState.Pending -> prevRoomNotificationSettings
is RoomNotificationSettingsState.Error -> prevRoomNotificationSettings
else -> null
}
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
sealed interface RoomType {
data object Space : RoomType
data object Room : RoomType
data class Other(val type: String) : RoomType
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
/**
* Try to find an existing DM with the given user, or create one if none exists and [createIfDmDoesNotExist] is true.
*/
suspend fun MatrixClient.startDM(
userId: UserId,
createIfDmDoesNotExist: Boolean,
): StartDMResult {
return findDM(userId)
.fold(
onSuccess = { existingDM ->
if (existingDM != null) {
StartDMResult.Success(existingDM, isNew = false)
} else if (createIfDmDoesNotExist) {
createDM(userId).fold(
{ StartDMResult.Success(it, isNew = true) },
{ StartDMResult.Failure(it) }
)
} else {
StartDMResult.DmDoesNotExist
}
},
onFailure = { error ->
StartDMResult.Failure(error)
}
)
}
sealed interface StartDMResult {
data class Success(val roomId: RoomId, val isNew: Boolean) : StartDMResult
data object DmDoesNotExist : StartDMResult
data class Failure(val throwable: Throwable) : StartDMResult
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
enum class StateEventType {
POLICY_RULE_ROOM,
POLICY_RULE_SERVER,
POLICY_RULE_USER,
CALL_MEMBER,
ROOM_ALIASES,
ROOM_AVATAR,
ROOM_CANONICAL_ALIAS,
ROOM_CREATE,
ROOM_ENCRYPTION,
ROOM_GUEST_ACCESS,
ROOM_HISTORY_VISIBILITY,
ROOM_JOIN_RULES,
ROOM_MEMBER_EVENT,
ROOM_NAME,
ROOM_PINNED_EVENTS,
ROOM_POWER_LEVELS,
ROOM_SERVER_ACL,
ROOM_THIRD_PARTY_INVITE,
ROOM_TOMBSTONE,
ROOM_TOPIC,
SPACE_CHILD,
SPACE_PARENT
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room.alias
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.BaseRoom
/**
* Return true if the given roomIdOrAlias is the same room as this room.
*/
fun BaseRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean {
return when (roomIdOrAlias) {
is RoomIdOrAlias.Id -> {
roomIdOrAlias.roomId == roomId
}
is RoomIdOrAlias.Alias -> {
roomIdOrAlias.roomAlias == info().canonicalAlias || roomIdOrAlias.roomAlias in info().alternativeAliases
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room.alias
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Information about a room, that was resolved from a room alias.
*/
data class ResolvedRoomAlias(
/**
* The room ID that the alias resolved to.
*/
val roomId: RoomId,
/**
* A list of servers that can be used to find the room by its room ID.
*/
val servers: List<String>
)

Some files were not shown because too many files have changed in this diff Show More