Merge pull request #7426 from element-hq/feature/bma/documentation

Add KDoc to all the interfaces of the api modules
This commit is contained in:
Benoit Marty
2026-08-14 13:46:28 +02:00
committed by GitHub
187 changed files with 2750 additions and 37 deletions
@@ -12,15 +12,34 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import kotlinx.coroutines.flow.Flow
/**
* Queues the full-screen announcements shown to the user once, such as the notice about a new feature.
*
* A dismissed announcement is remembered so it is not shown again.
*/
interface AnnouncementService {
/**
* Queues an announcement, unless the user has already dismissed it.
*
* @param announcement the announcement to show.
*/
suspend fun showAnnouncement(announcement: Announcement)
/**
* Records that the user dismissed an announcement, so it is not queued again.
*
* @param announcement the announcement that was dismissed.
*/
suspend fun onAnnouncementDismissed(announcement: Announcement)
/** The announcements still waiting to be shown, in the order they should appear. */
fun announcementsToShowFlow(): Flow<List<Announcement>>
/**
* Use this composable to render the announcement UI in Fullscreen.
* Draws nothing while there is no announcement to show.
*
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
@@ -8,6 +8,9 @@
package io.element.android.features.cachecleaner.api
/**
* Removes the temporary files holding decrypted content, so plaintext media does not linger on disk.
*/
interface CacheCleaner {
/**
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
@@ -10,6 +10,9 @@ package io.element.android.features.call.api
import kotlinx.coroutines.flow.StateFlow
/**
* Tracks the call the user is currently in on this device, so unrelated parts of the app can react to it.
*/
interface CurrentCallService {
/**
* The current call state flow, which will be updated when the active call changes.
@@ -18,5 +18,11 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
*/
fun interface ContentScannerService {
/**
* Starts scanning the given media, reporting the verdict of each one through [contentValidationState] rather than by returning.
*
* @param mediaSources the media to scan.
* @param contentValidationState where the outcome of each scan is published for the UI to observe.
*/
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
}
@@ -15,9 +15,21 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint {
/**
* Configures the create-room flow before it is started, since it serves rooms and spaces from the same screens.
*/
interface Builder {
/**
* @param isSpace true to create a space rather than a room.
*/
fun setIsSpace(isSpace: Boolean): Builder
/**
* @param parentSpaceId the space the new room should be added to once created.
*/
fun setParentSpace(parentSpaceId: RoomId): Builder
/** Builds the node with the options configured so far. */
fun build(): Node
}
@@ -15,11 +15,41 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
/**
* Exposes the restrictions and customisations an enterprise deployment can impose: allowed homeservers, branding, push gateways and more.
*
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
*/
interface EnterpriseService {
/** Whether this build is an enterprise one; `false` for the public app. */
val isEnterpriseBuild: Boolean
/**
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
*
* @param sessionId the session to check.
*/
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
/**
* Rewrites the authentication server URL when the deployment requires a different one from the one advertised.
*
* @param url the URL to rewrite.
* @param homeserver the homeserver the URL belongs to.
* @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works.
*/
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
/**
* The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction.
*/
fun defaultHomeserverList(): List<String>
/**
* Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList].
*
* @param homeserverUrl the server the user is trying to use.
*/
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
/**
@@ -29,17 +59,38 @@ interface EnterpriseService {
*/
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
/**
* The brand color to apply, or `null` when the default theme color should be used.
*
* @param sessionId the session to read the color of, or `null` for the color used when no session is active.
*/
fun brandColorsFlow(sessionId: SessionId?): Flow<Color?>
/**
* The full light and dark color palettes derived from the brand color.
*
* @param sessionId the session to read the colors of, or `null` for the colors used when no session is active.
*/
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
/** The push gateway to register with when using Firebase, or `null` to use the app default. */
fun firebasePushGateway(): String?
/** The push gateway to register with when using UnifiedPush, or `null` to use the app default. */
fun unifiedPushDefaultPushGateway(): String?
/**
* Where bug reports should be submitted, which an enterprise deployment can redirect or disable entirely.
*
* @param sessionId the session to read the setting of, or `null` for the setting used when no session is active.
*/
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
/**
* Gets Notification Channel to use for the noisy notifications of the provided session.
* Returns `null` when the session has no dedicated channel and the app-wide one should be used.
*
* @param sessionId the session whose channel is requested.
*/
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
@@ -50,6 +101,9 @@ interface EnterpriseService {
/**
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
* Returns `null` when this build does not read its configuration from an ESS deployment.
*
* @param domain the server whose configuration endpoint is requested.
*/
fun essConfigEndpointUrl(domain: String): String?
@@ -8,11 +8,25 @@
package io.element.android.features.enterprise.api
/**
* The session-scoped counterpart of [EnterpriseService], answering the same kind of questions for the session it belongs to.
*
* [init] must have run before the other members return meaningful answers.
*/
interface SessionEnterpriseService {
/** Whether Element Call may be used on this session, which a deployment can turn off. */
suspend fun isElementCallAvailable(): Boolean
/**
* Rewrites the authentication server URL for this session; see [EnterpriseService.tweakMasUrl].
*
* @param url the URL to rewrite.
*/
suspend fun tweakMasUrl(url: String): String
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
suspend fun isEncryptionDisabledByHomeserver(): Boolean
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
}
@@ -15,9 +15,21 @@ import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
*/
interface RemoteEnterpriseConfigProvider {
/**
* Returns the configuration of a session, served from the cache when possible.
* A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network.
*
* @param sessionId the session whose configuration is requested.
*/
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
/**
* Creates a provider, which needs a fetcher because the configuration is read over HTTP.
*/
interface Factory {
/**
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
*/
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
}
}
@@ -11,6 +11,9 @@ package io.element.android.features.invite.api
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
/**
* Remembers which invitations the user has already looked at, so that only genuinely new ones are highlighted in the room list.
*/
interface SeenInvitesStore {
/**
* Returns a flow of seen room IDs of invitation.
@@ -32,7 +35,7 @@ interface SeenInvitesStore {
suspend fun markAsUnSeen(roomId: RoomId)
/**
* Delete the store.
* Delete the store, so every invitation counts as unseen again.
*/
suspend fun clear()
}
@@ -10,6 +10,9 @@ package io.element.android.features.invite.api.acceptdecline
import io.element.android.features.invite.api.InviteData
/**
* Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation.
*/
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
@@ -12,7 +12,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Renders the confirmation dialogs and error states of accepting or declining an invitation; it draws nothing until such an event is sent.
*
* Embedded by every screen that can act on an invitation, so the behaviour stays identical between the invite list and a room preview.
*/
fun interface AcceptDeclineInviteView {
/**
* Draws whichever dialog the current state calls for.
*
* @param state the state produced by the accept/decline presenter.
* @param onAcceptInviteSuccess called once the room has been joined, so the host screen can navigate into it.
* @param onDeclineInviteSuccess called once the invitation has been declined.
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: AcceptDeclineInviteState,
@@ -8,6 +8,9 @@
package io.element.android.features.invitepeople.api
/**
* Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink].
*/
interface InvitePeopleEvents {
data object SendInvites : InvitePeopleEvents
data object CloseSearch : InvitePeopleEvents
@@ -13,7 +13,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
interface InvitePeoplePresenter : Presenter<InvitePeopleState> {
/**
* Creates a presenter for one invite flow.
*/
interface Factory {
/**
* @param joinedRoom the room to invite into, or `null` when the room does not exist yet, as when inviting from a direct message.
* @param roomId the id of the room to invite into.
*/
fun create(
joinedRoom: JoinedRoom?,
roomId: RoomId,
@@ -11,7 +11,16 @@ package io.element.android.features.invitepeople.api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Renders the invite people UI, so that other screens can embed it without depending on its implementation.
*/
interface InvitePeopleRenderer {
/**
* Draws the invite list and its search field.
*
* @param state the state produced by [InvitePeoplePresenter].
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: InvitePeopleState,
@@ -11,10 +11,22 @@ package io.element.android.features.invitepeople.api
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
/**
* State of the invite people UI, produced by an [InvitePeoplePresenter] and consumed by an [InvitePeopleRenderer].
*/
interface InvitePeopleState {
/** Whether the send button should be enabled, i.e. at least one user is selected and the user has the permission. */
val canInvite: Boolean
/** Whether the search field currently has focus, which the host screen uses to adapt its own layout. */
val isSearchActive: Boolean
/** Progress of the invite request, so the UI can show a loader and surface failures. */
val sendInvitesAction: AsyncAction<Unit>
/** Progress of turning a direct message into a room, which happens when inviting a third person into a DM. */
val createRoomFromDmAction: AsyncAction<RoomId>
/** Where the UI sends its events. */
val eventSink: (InvitePeopleEvents) -> Unit
}
@@ -11,7 +11,16 @@ package io.element.android.features.knockrequests.api.banner
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Renders the banner shown at the top of a room when people are waiting to join it.
*/
interface KnockRequestsBannerRenderer {
/**
* Draws the banner, or nothing when there is no pending request or the user cannot moderate the room.
*
* @param modifier layout modifier for the container.
* @param onViewRequestsClick called when the user asks to see the full list of requests.
*/
@Composable
fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
}
@@ -10,6 +10,9 @@ package io.element.android.features.leaveroom.api
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Events sent to the leave room presenter through [LeaveRoomState.eventSink].
*/
interface LeaveRoomEvent {
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent
}
@@ -12,7 +12,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Renders the confirmation dialogs of the leave room flow; it draws nothing until the host screen sends a leave event.
*/
fun interface LeaveRoomRenderer {
/**
* Draws whichever dialog the current state calls for.
*
* @param state the state produced by the leave room presenter.
* @param onSelectNewOwners called when the user is the last owner of a room that still has other members, and must promote someone before leaving.
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: LeaveRoomState,
@@ -10,7 +10,11 @@ package io.element.android.features.leaveroom.api
import androidx.compose.runtime.Immutable
/**
* State of the leave room flow, which is only the confirmation dialogs: the screen it is hosted in owns the rest of the UI.
*/
@Immutable
interface LeaveRoomState {
/** Where the host screen sends its events to start or confirm leaving a room. */
val eventSink: (LeaveRoomEvent) -> Unit
}
@@ -8,6 +8,10 @@
package io.element.android.features.location.api
/**
* Tells whether the location features can be used at all, which depends on the build having a map provider configured.
*/
interface LocationService {
/** Whether location sharing and map rendering are available; `false` disables the whole feature in the UI. */
fun isServiceAvailable(): Boolean
}
@@ -16,6 +16,17 @@ import io.element.android.libraries.wellknown.api.MapTilerConfig
* Builds an URL for a 3rd party service provider static maps API.
*/
interface StaticMapUrlBuilder {
/**
* Builds the URL of a map image centred on the given coordinates.
*
* @param lat latitude of the centre of the map.
* @param lon longitude of the centre of the map.
* @param zoom zoom level to render at.
* @param darkMode whether to request the dark map style.
* @param width width of the image in density independent pixels.
* @param height height of the image in density independent pixels.
* @param density screen density, used to request a matching pixel size.
*/
fun build(
lat: Double,
lon: Double,
@@ -26,6 +37,7 @@ interface StaticMapUrlBuilder {
density: Float,
): String
/** Whether a map provider is configured for this build; the URLs are unusable when it is not. */
fun isServiceAvailable(): Boolean
}
@@ -19,6 +19,12 @@ import io.element.android.libraries.wellknown.api.MapTilerConfig
* Used for rendering dynamic maps.
*/
interface TileServerStyleUriBuilder {
/**
* Builds the style URI the map view loads its tiles from.
*
* @param customMapStyleUrl the style advertised by the homeserver, or `null` to use the one built into the app.
* @param darkMode whether to request the dark map style.
*/
fun build(
customMapTilerConfig: MapTilerConfig?,
darkMode: Boolean,
@@ -12,6 +12,9 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.StateFlow
import kotlin.time.Duration
/**
* Owns the live location shares running on this device, across every room, and the foreground service that feeds them GPS updates.
*/
interface ActiveLiveLocationShareManager {
/** All rooms currently sharing live location on this device. */
val sharingRoomIds: StateFlow<Set<RoomId>>
@@ -27,6 +30,9 @@ interface ActiveLiveLocationShareManager {
* Starts live location sharing in the given room.
* Calls room.startLiveLocationShare() on the SDK, registers the share,
* and starts the foreground GPS service if not already running.
*
* @param roomId the room to start sharing the location in.
* @param duration how long the share should last before it expires on its own.
*/
suspend fun startShare(roomId: RoomId, duration: Duration): Result<Unit>
@@ -34,6 +40,8 @@ interface ActiveLiveLocationShareManager {
* Stops live location sharing in the given room.
* Calls room.stopLiveLocationShare() on the SDK, removes the share,
* and stops the foreground service if no shares remain.
*
* @param roomId the room to stop sharing the location in.
*/
suspend fun stopShare(roomId: RoomId): Result<Unit>
}
@@ -17,6 +17,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* Exposes the app lock state: whether a PIN or biometric lock is set up, whether it is mandatory, and whether the app is locked right now.
*/
interface LockScreenService {
/**
* The current lock state of the app.
@@ -8,6 +8,13 @@
package io.element.android.features.login.api
/**
* Extracts the login parameters from a link, so that a deployment can hand out a URL that pre-fills the sign-in screen.
*/
interface LoginIntentResolver {
/**
* @param uriString the link the app was opened with.
* @return the account provider and login hint to pre-fill, or `null` when the link is not a login link.
*/
fun parse(uriString: String): LoginParams?
}
@@ -8,6 +8,14 @@
package io.element.android.features.login.api.accesscontrol
/**
* Enforces the restriction an enterprise deployment can put on which account providers the user may sign in to.
*/
interface AccountProviderAccessControl {
/**
* Whether sign-in to this provider is permitted; `true` on a build with no such restriction.
*
* @param accountProviderUrl the server the user is trying to use.
*/
suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String): Boolean
}
@@ -10,7 +10,17 @@ package io.element.android.features.logout.api.direct
import androidx.compose.runtime.Composable
/**
* Renders the confirmation dialogs of logging out directly, in particular the warning shown when the user would lose their room keys.
*
* It draws nothing until the host screen sends a logout event.
*/
fun interface DirectLogoutView {
/**
* Draws whichever dialog the current state calls for.
*
* @param state the state produced by the direct logout presenter.
*/
@Composable
fun Render(state: DirectLogoutState)
}
@@ -17,5 +17,6 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
* the composer is in a thread, if it's editing a message, etc.
*/
interface MessageComposerContext {
/** What the composer is currently doing: writing a new message, editing one, replying, or composing in a thread. */
val composerMode: MessageComposerMode
}
@@ -50,7 +50,16 @@ interface MessagesEntryPoint : FeatureEntryPoint {
callback: Callback,
): Node
/**
* Lets callers outside this feature drive an already created messages node, for navigation that has to happen after it is attached.
*/
interface NodeProxy {
/**
* Opens a thread on top of the messages screen, suspending until the thread node is attached.
*
* @param threadId the thread to open.
* @param focusedEventId the event to scroll to within the thread, or `null` to open at the latest message.
*/
suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?)
}
}
@@ -10,4 +10,9 @@ package io.element.android.features.messages.api.pinned
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
/**
* A [TimelineProvider] whose active timeline holds the pinned events of the room, rather than its live events.
*
* It exists as its own type so that it can be injected where only the pinned timeline is wanted.
*/
interface PinnedEventsTimelineProvider : TimelineProvider
@@ -11,9 +11,16 @@ package io.element.android.features.messages.api.timeline
import androidx.compose.runtime.Composable
import io.element.android.wysiwyg.utils.HtmlConverter
/**
* Provides the converter that turns message HTML into styled text.
*
* The converter depends on the current theme and typography, so [Update] must be composed before [provide] is called.
*/
interface HtmlConverterProvider {
/** Rebuilds the converter from the current Compose theme; call this once, high in the composition of a screen that renders messages. */
@Composable
fun Update()
/** Returns the converter built by the last [Update] call. */
fun provide(): HtmlConverter
}
@@ -12,7 +12,13 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.timeline.Timeline
fun interface VoiceMessageComposerPresenter : Presenter<VoiceMessageComposerState> {
/**
* Creates a presenter for one voice message composer.
*/
interface Factory {
/**
* @param timelineMode the timeline the recording will be sent to, which matters for threads in particular.
*/
fun create(timelineMode: Timeline.Mode): VoiceMessageComposerPresenter
}
}
@@ -11,6 +11,13 @@ package io.element.android.features.poll.api.actions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
/**
* Closes a poll so no further answers are accepted, wrapping the timeline call so the analytics are captured in one place.
*/
interface EndPollAction {
/**
* @param timeline the timeline the poll lives in.
* @param pollStartId the poll start event to close.
*/
suspend fun execute(timeline: Timeline, pollStartId: EventId): Result<Unit>
}
@@ -11,7 +11,15 @@ package io.element.android.features.poll.api.actions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
/**
* Sends the current user's answer to a poll, wrapping the timeline call so the analytics are captured in one place.
*/
interface SendPollResponseAction {
/**
* @param timeline the timeline the poll lives in.
* @param pollStartId the poll start event being answered.
* @param answerId the id of the chosen answer.
*/
suspend fun execute(
timeline: Timeline,
pollStartId: EventId,
@@ -12,7 +12,16 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
/**
* Turns the poll content of a timeline event into the state the poll UI renders.
*/
interface PollContentStateFactory {
/**
* Convenience overload that reads the editability and ownership from the timeline item itself.
*
* @param eventTimelineItem the timeline item carrying the poll.
* @param content the poll content of that item.
*/
suspend fun create(eventTimelineItem: EventTimelineItem, content: PollContent): PollContentState {
return create(
eventId = eventTimelineItem.eventId,
@@ -21,5 +30,12 @@ interface PollContentStateFactory {
content = content,
)
}
/**
* @param eventId the poll start event, or `null` while the poll is still a local echo.
* @param isEditable whether the current user may still edit the poll.
* @param isOwn whether the poll was created by the current user.
* @param content the poll question, answers and votes.
*/
suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState
}
@@ -11,6 +11,9 @@ package io.element.android.features.preferences.api
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
/**
* Announces that a session's cache has been cleared, since clearing it destroys the client and the app has to react.
*/
interface CacheService {
/**
* A flow of [SessionId], can let the app to know when the
@@ -14,6 +14,11 @@ import androidx.compose.ui.Modifier
* Component for rendering extra developer options in the preferences screen.
*/
interface ExtraDeveloperOptionsRenderer {
/**
* Draws the extra options. The standard build binds a no-op implementation, so only the builds that actually add options provide a real one.
*
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(modifier: Modifier)
}
@@ -10,6 +10,10 @@ package io.element.android.features.rageshake.api
import kotlinx.coroutines.flow.Flow
/**
* Tells whether bug reporting is available, which an enterprise deployment can turn off.
*/
fun interface RageshakeFeatureAvailability {
/** Whether the rageshake and bug report entry points should be offered to the user. */
fun isAvailable(): Flow<Boolean>
}
@@ -10,6 +10,9 @@ package io.element.android.features.rageshake.api.logs
import java.io.File
/**
* Deletes the log files the app writes, used to keep them from growing without bound and to clear them on request.
*/
interface LogFilesRemover {
/**
* Perform the log files removal.
@@ -10,6 +10,11 @@ package io.element.android.features.rageshake.api.reporter
import java.io.File
/**
* Uploads bug reports, including the log files the app has been writing.
*
* Progress and the outcome are reported through the listener passed to [sendBugReport] rather than by returning or throwing.
*/
interface BugReporter {
/**
* Send a bug report.
@@ -35,17 +40,21 @@ interface BugReporter {
)
/**
* Provide the log directory.
* Provide the log directory, i.e. where the rotating log files are written.
*/
fun logDirectory(): File
/**
* Set the current tracing log level.
* Set the current tracing log level, so it can be reported alongside the bug.
*
* @param logLevel the log level currently in effect.
*/
fun setCurrentTracingLogLevel(logLevel: String)
/**
* Save the logcat.
*
* @return the file the logcat was written to, or `null` when it could not be captured.
*/
fun saveLogCat(): File?
}
@@ -22,8 +22,18 @@ fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
listType: ChangeRoomMemberRolesListType,
): Node
/**
* Lets a caller outside this feature wait for the role change to finish, which is needed when leaving a room depends on it.
*/
interface NodeProxy {
/** The room whose member roles are being changed. */
val roomId: RoomId
/**
* Suspends until the user finishes or abandons the flow.
*
* @return true when roles were actually changed, false when the user backed out.
*/
suspend fun waitForCompletion(): Boolean
}
}
@@ -10,6 +10,9 @@ package io.element.android.features.roommembermoderation.api
import io.element.android.libraries.matrix.api.user.MatrixUser
/**
* Events sent to the moderation presenter through [RoomMemberModerationState.eventSink].
*/
interface RoomMemberModerationEvents {
data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents
data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvents
@@ -12,7 +12,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.matrix.api.user.MatrixUser
/**
* Renders the moderation action sheet and its confirmation dialogs; it draws nothing until the host screen asks for a member's actions.
*/
interface RoomMemberModerationRenderer {
/**
* Draws whichever sheet or dialog the current state calls for.
*
* @param state the state produced by the moderation presenter.
* @param onSelectAction called when the user picks an action, so the host screen can navigate if that action needs its own screen.
* @param onAvatarClick called when the user taps the member's avatar, or `null` to make it non-interactive.
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
state: RoomMemberModerationState,
@@ -10,9 +10,15 @@ package io.element.android.features.roommembermoderation.api
import androidx.compose.runtime.Immutable
/**
* State of the member moderation flow, which is the action sheet and the confirmation dialogs shown for a room member.
*/
@Immutable
interface RoomMemberModerationState {
/** Which moderation actions the current user is allowed to perform, so the UI can hide or disable the rest. */
val permissions: RoomMemberModerationPermissions
/** Where the host screen sends its events to open the actions of a member or run one of them. */
val eventSink: (RoomMemberModerationEvents) -> Unit
}
@@ -11,5 +11,8 @@ package io.element.android.features.share.api
* Post-processing to be done once a [ShareIntentData] has been consumed.
*/
fun interface OnSharedData {
/**
* @param data the shared content that has just been handled, so any temporary copy of it can be released.
*/
operator fun invoke(data: ShareIntentData)
}
@@ -9,10 +9,14 @@ package io.element.android.features.share.api
import android.content.Intent
/**
* Recognises the intents another app uses to share content into this one, and resolves what was shared.
*/
interface ShareIntentHandler {
/**
* This methods aims to handle incoming share intents and parse its data.
*
* @param intent the intent the app was started with.
* @return the [ShareIntentData] if it could be resolved, or null.
*/
fun handleIncomingShareIntent(
@@ -13,6 +13,11 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
/**
* Opens a direct message with a user, reusing the existing room when there is one.
*
* Progress is reported into the state passed in, so the caller only has to render it.
*/
interface StartDMAction {
/**
* Try to find an existing DM with the given user, or create one if none exists.
@@ -11,6 +11,12 @@ package io.element.android.features.userprofile.api
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId
/**
* Creates the user profile presenter, so that other features can show a profile without depending on this one's implementation.
*/
fun interface UserProfilePresenterFactory {
/**
* @param userId the user whose profile is shown.
*/
fun create(userId: UserId): Presenter<UserProfileState>
}
@@ -12,7 +12,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import kotlinx.collections.immutable.ImmutableList
/**
* Renders the contents of a text file, used by the log viewer in the developer options.
*/
fun interface TextFileViewer {
/**
* Draws the file, one row per line.
*
* @param lines the already-read contents of the file.
* @param modifier layout modifier for the container.
*/
@Composable
fun Render(
lines: ImmutableList<String>,
@@ -15,6 +15,9 @@ enum class AudioFocusRequester {
MediaViewer,
}
/**
* Claims and releases system audio focus, so that the app's own players do not talk over each other or over other apps.
*/
interface AudioFocus {
/**
* Request audio focus for the given requester.
@@ -7,9 +7,33 @@
package io.element.android.libraries.cachestore.api
/**
* A generic on-disk key/value cache, where each entry records when it was written so callers can decide for themselves when it is stale.
*/
interface CacheStore {
/**
* Stores an entry, replacing any previous one under the same key.
*
* @param key the entry to write.
* @param data the value along with the time it was obtained.
*/
suspend fun storeData(key: String, data: CacheData)
/**
* Reads an entry without applying any expiry of its own; judging staleness is up to the caller.
*
* @param key the entry to read.
* @return the value and its timestamp, or `null` when nothing is stored under that key.
*/
suspend fun getData(key: String): CacheData?
/**
* Removes one entry.
*
* @param key the entry to delete.
*/
suspend fun deleteData(key: String)
/** Empties the whole cache. */
suspend fun deleteAll()
}
@@ -15,8 +15,35 @@ import javax.crypto.SecretKey
* Simple service to provide encryption and decryption operations.
*/
interface EncryptionDecryptionService {
/**
* Creates a cipher ready to encrypt, for the callers that need the cipher itself, such as to bind it to a biometric prompt.
*
* @param key the key to encrypt with.
*/
fun createEncryptionCipher(key: SecretKey): Cipher
/**
* Creates a cipher ready to decrypt, for the callers that need the cipher itself.
*
* @param key the key to decrypt with.
* @param initializationVector the vector produced when the data was encrypted.
*/
fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher
/**
* Encrypts data in one call.
*
* @param key the key to encrypt with.
* @param input the plaintext bytes.
* @return the ciphertext along with the initialization vector needed to decrypt it.
*/
fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult
/**
* Decrypts data in one call.
*
* @param key the key the data was encrypted with.
* @param encryptionResult the ciphertext and its initialization vector, as returned by [encrypt].
*/
fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray
}
@@ -16,10 +16,17 @@ import javax.crypto.SecretKey
* Implementation should be able to store the generated key securely.
*/
interface SecretKeyRepository {
/**
* Whether a key exists for the given alias, re-emitted when it is created or deleted.
*
* @param alias the alias to watch.
*/
fun hasKey(alias: String): Flow<Boolean>
/**
* Get or create a secret key for a given alias.
* Note that [requiresUserAuthentication] only applies when the key is created: an existing key keeps the protection it was created with.
*
* @param alias the alias to use
* @param requiresUserAuthentication true if the key should be protected by user authentication
*/
@@ -8,7 +8,17 @@
package io.element.android.libraries.dateformatter.api
/**
* Formats timestamps for display, in the user's locale and time zone.
*/
interface DateFormatter {
/**
* Formats a timestamp according to [mode].
*
* @param timestamp milliseconds since the epoch; a `null` value yields an empty string, so callers need no null check of their own.
* @param mode how much of the date and time to include; see [DateFormatterMode] for examples of each.
* @param useRelative true to prefer wording relative to today, such as "Today" or "This month", where the mode allows it.
*/
fun format(
timestamp: Long?,
mode: DateFormatterMode = DateFormatterMode.Full,
@@ -21,6 +21,11 @@ import kotlin.time.Duration
* - 30 seconds → "30 seconds"
*/
interface DurationFormatter {
/**
* Formats the duration using its largest appropriate unit, rounding rather than listing several units.
*
* @param duration the duration to describe.
*/
fun format(duration: Duration): String
}
@@ -13,6 +13,17 @@ 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
/**
* Builds the app's internal deep links, used to point a notification or a shortcut at a precise place in the app.
*/
fun interface DeepLinkCreator {
/**
* Builds a link to the most precise target the arguments allow, ignoring the deeper ones once an outer one is `null`.
*
* @param sessionId the session to open.
* @param roomId the room to open, or `null` to stop at the room list.
* @param threadId the thread to open within that room, or `null` for the main timeline.
* @param eventId the event to focus on, or `null` to open at the latest message.
*/
fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?, eventId: EventId?): String
}
@@ -10,6 +10,13 @@ package io.element.android.libraries.deeplink.api
import android.content.Intent
/**
* Extracts the app's own deep link data from an incoming intent; see [DeepLinkCreator] for the other direction.
*/
fun interface DeeplinkParser {
/**
* @param intent the intent the app was started or resumed with.
* @return the session, room and event to open, or `null` when the intent is not one of the app's deep links.
*/
fun getFromIntent(intent: Intent): DeeplinkData?
}
@@ -10,6 +10,12 @@ package io.element.android.libraries.deeplink.api.usecase
import android.app.Activity
/**
* Opens the system share sheet with an invitation to install the app.
*/
fun interface InviteFriendsUseCase {
/**
* @param activity the activity the share sheet is shown from.
*/
fun execute(activity: Activity)
}
@@ -11,7 +11,14 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
fun interface EmojiPickerPresenter : Presenter<EmojiPickerState> {
/**
* Creates a presenter for one picker instance.
*/
fun interface Factory {
/**
* @param getRecentEmojis how the picker sources its recent tab; pass [io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis]
* for a picker that should have no recent tab at all.
*/
fun create(getRecentEmojis: GetRecentEmojis): EmojiPickerPresenter
}
}
@@ -13,9 +13,16 @@ import androidx.compose.ui.Modifier
import io.element.android.emojibasebindings.Emoji
import kotlinx.collections.immutable.ImmutableSet
/**
* Renders the emoji picker, so that callers can embed it without depending on its implementation.
*
* See [NoOpEmojiPickerRenderer] for a renderer that draws nothing, used where the picker is disabled.
*/
@Immutable
interface EmojiPickerRenderer {
/**
* Draws the picker.
*
* @param state opaque state produced by [EmojiPickerPresenter.present].
* @param onSelectEmoji invoked when the user taps an emoji (including a skin-tone variant).
* @param selectedEmojis emojis to visually mark as already selected.
@@ -8,6 +8,12 @@
package io.element.android.libraries.emoji.api.recentemojis
/**
* Records that an emoji was just used, so it surfaces in the picker's recent list; see [GetRecentEmojis] for the read side.
*/
fun interface AddRecentEmoji {
/**
* @param emoji the emoji that was just used, as its unicode representation.
*/
suspend operator fun invoke(emoji: String): Result<Unit>
}
@@ -15,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf
* Returns the list of recently used emojis for reactions.
*/
fun interface GetRecentEmojis {
/** Returns the recently used emojis, most recent first; see [EmptyGetRecentEmojis] for a picker with no recent tab. */
suspend operator fun invoke(): Result<ImmutableList<String>>
}
@@ -10,6 +10,12 @@ package io.element.android.libraries.eventformatter.api
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
/**
* Formats the short summary of a pinned event shown in the banner at the top of a room.
*/
interface PinnedMessagesBannerFormatter {
/**
* @param event the pinned event to describe.
*/
fun format(event: EventTimelineItem): CharSequence
}
@@ -10,7 +10,25 @@ package io.element.android.libraries.eventformatter.api
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
/**
* Formats the one-line preview of a room's latest event, as shown in the room list.
*/
interface RoomLatestEventFormatter {
/**
* Formats an event that is still local, i.e. queued or being sent.
*
* @param latestEvent the local event to describe.
* @param isDmRoom whether the room is a direct message, which lets the sender name be left out.
* @return the preview text, or `null` when this kind of event should not be previewed.
*/
fun format(latestEvent: LatestEventValue.Local, isDmRoom: Boolean): CharSequence?
/**
* Formats an event that has been received from the server.
*
* @param latestEvent the remote event to describe.
* @param isDmRoom whether the room is a direct message, which lets the sender name be left out.
* @return the preview text, or `null` when this kind of event should not be previewed.
*/
fun format(latestEvent: LatestEventValue.Remote, isDmRoom: Boolean): CharSequence?
}
@@ -13,7 +13,16 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
/**
* Turns a state or membership event into the human readable sentence shown in the timeline, such as "Alice joined the room".
*/
interface TimelineEventFormatter {
/**
* Convenience overload that reads the sender and ownership from the timeline item itself.
*
* @param event the timeline item to describe.
* @return the sentence to display, or `null` when this kind of event is not rendered as text.
*/
fun format(event: EventTimelineItem): CharSequence? {
return format(
content = event.content,
@@ -22,5 +31,13 @@ interface TimelineEventFormatter {
senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
)
}
/**
* @param content the event content to describe.
* @param isOutgoing whether the event was sent by the current user, which selects the first-person wording.
* @param sender the user who sent the event.
* @param senderDisambiguatedDisplayName the sender's display name, already suffixed with their id when it is ambiguous in the room.
* @return the sentence to display, or `null` when this kind of event is not rendered as text.
*/
fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence?
}
@@ -10,6 +10,9 @@ package io.element.android.libraries.featureflag.api
import io.element.android.libraries.core.meta.BuildMeta
/**
* Describes one feature flag: how it is identified, how it is presented in the developer options, and what it defaults to.
*/
interface Feature {
/**
* Unique key to identify the feature.
@@ -11,6 +11,9 @@ package io.element.android.libraries.featureflag.api
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
/**
* Reads and, where a mutable provider is registered, changes the state of the feature flags.
*/
interface FeatureFlagService {
/**
* @param feature the feature to check for
@@ -15,9 +15,11 @@ import androidx.compose.runtime.State
* A set of State<Boolean> to observe to display or not the indicators in the UI.
*/
interface IndicatorService {
/** Whether to show the red dot on the room list top bar, which aggregates the reasons the user should open the settings. */
@Composable
fun showRoomListTopBarIndicator(): State<Boolean>
/** Whether to show the red dot on the chat backup setting, i.e. when recovery needs the user's attention. */
@Composable
fun showSettingChatBackupIndicator(): State<Boolean>
}
@@ -10,6 +10,8 @@ package io.element.android.libraries.matrix.api
/**
* Provides information about the capabilities of the homeserver.
*
* The getters read a snapshot held by the SDK rather than querying the server on each call, so [refresh] must be used to pick up server-side changes.
*
* Spec: https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation
*/
interface HomeserverCapabilitiesProvider {
@@ -54,47 +54,189 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.util.Optional
/**
* Entry point for everything scoped to a single logged in session: the sub-services, room access, and account level operations.
*
* One instance exists per session, created on login or session restore and torn down by [logout], [clearCache] or account deactivation.
* Suspending members here report failures through a [Result] instead of throwing, and run their work off the main thread.
*/
interface MatrixClient : UrlContentFetcher {
/** Identifier of the logged in user; a [SessionId] and a [UserId] are the same value. */
val sessionId: SessionId
/** Identifier of this device, as assigned by the homeserver when the session was created. */
val deviceId: DeviceId
/** Base URL of the homeserver this client is connected to. */
val homeserverUrl: String
/** Directories holding this session's databases, caches and downloaded media. */
val sessionPaths: SessionPaths
/**
* Display name and avatar of the logged in user.
* Seeded from the local session store so a value is available before the first network fetch, then kept up to date by the SDK.
*/
val userProfile: StateFlow<MatrixUser>
/** Gives access to the list of rooms the user is a member of, or has been invited to. */
val roomListService: RoomListService
/** Gives access to the spaces the user belongs to and their hierarchy. */
val spaceService: SpaceService
/** Controls the sync loop of this session and exposes its state. */
val syncService: SyncService
/** Handles verifying this session against the user's other devices or their recovery key. */
val sessionVerificationService: SessionVerificationService
/** Manages the push gateways (pushers) registered on the homeserver for this session. */
val pushersService: PushersService
/** Resolves incoming push notifications into renderable events. */
val notificationService: NotificationService
/** Reads and writes the push notification rules of the account. */
val notificationSettingsService: NotificationSettingsService
/** Gives access to end-to-end encryption state, recovery and key backup. */
val encryptionService: EncryptionService
/** Gives access to the public room directory of the homeserver. */
val roomDirectoryService: RoomDirectoryService
/** Whether this live client was built with a message search index attached. */
val isMessageSearchAvailable: Boolean
/** Searches messages in the local index; only usable when [isMessageSearchAvailable] is `true`. */
val messageSearchService: MessageSearchService
/** Reads and writes the account settings controlling whether media previews and avatars are shown. */
val mediaPreviewService: MediaPreviewService
/** Downloads media content, thumbnails and files from the homeserver, caching them on disk. */
val matrixMediaLoader: MatrixMediaLoader
/**
* Coroutine scope tied to the lifetime of this session, to be used for work that must stop when the session goes away.
* It is cancelled when the session is logged out or otherwise destroyed, so never use it for work that must outlive the session.
*/
val sessionCoroutineScope: CoroutineScope
/** The users the logged in user has ignored; starts empty and is updated by the SDK, including with an initial value. */
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
/** Notifies of membership changes the current user makes, so that callers can react to leaving or joining a room. */
val roomMembershipObserver: RoomMembershipObserver
/** Emits updates about the live location beacons published by the current user. */
val ownBeaconInfoUpdates: Flow<BeaconInfoUpdate>
/** Scans media content before it is displayed, or `null` when no content scanner is configured for this session. */
val contentScanner: ContentScanner?
/**
* Returns the room with the given id if the user has joined it, along with its live [Timeline].
* A new instance is created on each call and the caller owns it, so it must be closed once no longer needed.
*
* @param roomId the id of the room to open.
* @return the room, or `null` if it is unknown to the client or the user is not joined.
*/
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
/**
* Returns the room with the given id whatever the current membership is, without building a timeline.
* As with [getJoinedRoom], a new instance is created on each call and must be closed by the caller.
*
* @param roomId the id of the room to open.
* @return the room, or `null` if it is unknown to the client.
*/
suspend fun getRoom(roomId: RoomId): BaseRoom?
/**
* Looks for an existing direct message room shared with [userId].
*
* @param userId the other member of the direct message room.
* @return the room id, or `null` if no direct message room exists with this user.
*/
suspend fun findDM(userId: UserId): Result<RoomId?>
/**
* Returns the ids of every room the user has joined, as currently known by the client.
* Invited, knocked and left rooms are excluded.
*/
suspend fun getJoinedRoomIds(): Result<Set<RoomId>>
/**
* Adds [userId] to the ignore list of the account, which hides their events and makes [ignoredUsersFlow] emit.
*
* @param userId the user to ignore.
*/
suspend fun ignoreUser(userId: UserId): Result<Unit>
/**
* Removes [userId] from the ignore list of the account, which makes [ignoredUsersFlow] emit.
*
* @param userId the user to stop ignoring.
*/
suspend fun unignoreUser(userId: UserId): Result<Unit>
/**
* Creates a room, applying the app's default power levels on top of [createRoomParams].
* Waits a short while for the new room to come back through sync, but a timeout there is only logged: the result is still a success.
*
* @param createRoomParams the name, topic, preset, invitees and other settings of the room to create.
*/
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
/**
* Creates a direct message room with [userId], as a private, invite-only room with the user already invited.
* This does not check whether such a room already exists; use [findDM] first if you want to reuse one.
*
* @param userId the user to start the direct message with.
* @param isEncrypted whether the room should have end-to-end encryption enabled.
*/
suspend fun createDM(userId: UserId, isEncrypted: Boolean): Result<RoomId>
/**
* Fetches the current profile of any user from the homeserver, bypassing the local cache.
*
* @param userId the user whose profile is requested.
*/
suspend fun getProfile(userId: UserId): Result<MatrixUser>
/**
* Searches the homeserver's user directory.
*
* @param searchTerm the text to look for in user ids and display names.
* @param limit the maximum number of results to return.
*/
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
/**
* Changes the display name of the logged in user on the homeserver.
*
* @param displayName the new display name.
*/
suspend fun setDisplayName(displayName: String): Result<Unit>
/**
* Uploads [data] as the new avatar of the logged in user.
*
* @param mimeType the MIME type of the image, for instance `image/jpeg`.
* @param data the raw bytes of the image.
*/
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
/** Removes the avatar of the logged in user. */
suspend fun removeAvatar(): Result<Unit>
/**
* Sets the `m.status` profile field of the logged in user (MSC4426), refreshing [userProfile] if the server does not push the change itself.
*
* @param status the status to publish.
*/
suspend fun setUserStatus(status: UserStatus): Result<Unit>
/** Clears both m.status and m.call profile fields (maps to DELETE on the profile endpoint per MSC4426). */
@@ -103,16 +245,52 @@ interface MatrixClient : UrlContentFetcher {
/** Whether the homeserver advertises support for user status (MSC4426). */
suspend fun isUserStatusSupported(): Result<Boolean>
/** Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call. */
/**
* Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call.
*
* @param enabled whether the status should be maintained automatically.
*/
fun enableAutomaticCallStatus(enabled: Boolean)
/**
* Joins a room the client already knows about, then waits briefly for the new membership to arrive through sync.
* The returned [RoomInfo] is `null` when that wait times out, which is not treated as a failure: the join itself succeeded.
*
* @param roomId the id of the room to join.
*/
suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?>
/**
* Joins a room by id or by alias, resolving it through [serverNames] when the client does not know it yet.
* As with [joinRoom], the returned [RoomInfo] is `null` if the membership did not arrive through sync in time.
*
* @param roomIdOrAlias the id or the alias of the room to join.
* @param serverNames servers to ask about the room, used when it cannot be resolved locally.
*/
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomInfo?>
/**
* Sends a request to join a room whose join rule is knock, then waits briefly for the knocked membership to arrive through sync.
* As with [joinRoom], the returned [RoomInfo] is `null` if that wait times out.
*
* @param roomIdOrAlias the id or the alias of the room to knock on.
* @param message the reason shown to the room moderators; may be empty.
* @param serverNames servers to ask about the room, used when it cannot be resolved locally.
*/
suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomInfo?>
/**
* Returns the number of bytes this session occupies on disk that [clearCache] would be able to reclaim.
* This covers the cache directory, the state database and the message search index, but not the crypto database.
*/
suspend fun getCacheSize(): Long
/** Returns the size of each individual store the SDK maintains for this session, as reported by the SDK. */
suspend fun getDatabaseSizes(): Result<SdkStoreSizes>
/**
* Will close the client and delete the cache data.
* Deletes the cached data of this session and destroys the client.
* The instance is unusable afterwards, so callers are expected to restart the session, unlike with [logout] the session itself is kept.
*/
suspend fun clearCache()
@@ -125,29 +303,56 @@ interface MatrixClient : UrlContentFetcher {
suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean)
/**
* Retrieve the user profile, will also eventually emit a new value to [userProfile].
* Retrieve the profile of the logged in user, emitting it to [userProfile] and persisting it in the local session store on success.
*/
suspend fun getUserProfile(): Result<MatrixUser>
/**
* Returns the web URL where the user can manage their account on the authentication provider, or `null` if the homeserver exposes none.
*
* @param action an optional section of the account management page to open directly, such as changing the password.
*/
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
/**
* Uploads arbitrary content to the homeserver's media repository.
*
* @param mimeType the MIME type of the content being uploaded.
* @param data the raw bytes to upload.
* @return the `mxc://` URI the content is now available at.
*/
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.
* The flow will emit a new value whenever the room info is updated, skipping duplicates, and an empty [Optional] if the room is not found.
*
* @param roomId the id of the room to observe.
*/
fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<RoomInfo>>
/**
* Whether [userId] is the logged in user, `false` for a `null` value.
*
* @param userId the user id to compare against this session's user.
*/
fun isMe(userId: UserId?) = userId == sessionId
/**
* Adds a room to the account's list of recently visited rooms, which backs the room suggestions when sharing or forwarding.
*
* @param roomId the id of the room that was just visited.
*/
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
/** Returns the recently visited rooms of the account, most recent first. */
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.
*
* @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>>
@@ -158,6 +363,8 @@ interface MatrixClient : UrlContentFetcher {
* 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.
*
* @param enabled whether the send queues of every room should be enabled.
*/
suspend fun setAllSendQueuesEnabled(enabled: Boolean)
@@ -175,6 +382,9 @@ interface MatrixClient : UrlContentFetcher {
/**
* 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.
*
* @param roomIdOrAlias the id or the alias of the room to preview.
* @param serverNames servers to ask about the room, used when it cannot be resolved locally.
*/
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<NotJoinedRoom>
@@ -183,11 +393,21 @@ interface MatrixClient : UrlContentFetcher {
*/
suspend fun currentSlidingSyncVersion(): Result<SlidingSyncVersion>
/** Whether this account can be deactivated from the app, which depends on the authentication method used by the homeserver. */
fun canDeactivateAccount(): Boolean
/**
* Deactivates the account on the homeserver, then destroys this client and deletes every local trace of the session.
* The password is only sent if the homeserver asks for re-authentication, and on failure the session is left untouched.
*
* @param password the account password, required to confirm the deactivation.
* @param eraseData whether the homeserver should also erase the messages the user sent.
*/
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
/**
* Check if the user can report a room.
* Whether the homeserver supports reporting a whole room, rather than individual events.
* Returns `false` if the capability cannot be determined.
*/
suspend fun canReportRoom(): Boolean
@@ -208,16 +428,23 @@ interface MatrixClient : UrlContentFetcher {
/**
* Adds an emoji to the list of recent emoji reactions for this account.
*
* @param emoji the emoji that was just used.
*/
suspend fun addRecentEmoji(emoji: String): Result<Unit>
/**
* Returns the raw JSON content of the global account data event of type [eventType], or null if it is not set.
*
* @param eventType the Matrix event type of the account data to read, for instance `m.direct`.
*/
suspend fun getAccountData(eventType: String): Result<String?>
/**
* Sets the global account data event of type [eventType] to the raw JSON [content].
*
* @param eventType the Matrix event type of the account data to write.
* @param content the new content, as a raw JSON object.
*/
suspend fun setAccountData(eventType: String, content: String): Result<Unit>
@@ -226,6 +453,9 @@ interface MatrixClient : UrlContentFetcher {
*
* This method should be used with caution as providing the [eventId] ourselves can result in incorrect read receipts.
* Use [Timeline.markAsRead] instead when possible.
*
* @param roomId the id of the room to mark as read.
* @param eventId the event up to which the room is considered read.
*/
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
@@ -252,6 +482,7 @@ interface MatrixClient : UrlContentFetcher {
/**
* Performs a database optimization that should flush cached data and improve performance.
* A periodic background job already calls this for every session, so callers rarely need to trigger it by hand.
*/
suspend fun performDatabaseVacuum(): Result<Unit>
@@ -260,6 +491,7 @@ interface MatrixClient : UrlContentFetcher {
*/
suspend fun resetWellKnownConfig(): Result<Unit>
/** Returns a provider for the capabilities the homeserver advertises, such as whether the display name can be changed. */
fun homeserverCapabilities(): HomeserverCapabilitiesProvider
}
@@ -10,18 +10,26 @@ package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.SessionId
/**
* In-memory cache of the [MatrixClient] of every logged in session.
*
* Prefer injecting the [MatrixClient] directly: this provider is meant for the entry points that only know a [SessionId], such as push handling and workers.
*/
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.
* Returns the cached [MatrixClient] for [sessionId], restoring the session from storage and starting its sync if it is not in memory yet.
* Concurrent calls are serialised, so only one restore happens per session.
*
* @param sessionId the [SessionId] of the session to get or restore.
* @return the client, or a failure if the session could not be restored.
*/
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.
* Returns the [MatrixClient] for [sessionId] only if it is already in memory, without attempting to restore it.
*
* @param sessionId the [SessionId] of the session to retrieve.
* @return the cached client, or `null` if that session is not currently held in memory.
*/
fun getOrNull(sessionId: SessionId): MatrixClient?
}
@@ -8,6 +8,14 @@
package io.element.android.libraries.matrix.api
/**
* Exposes build information about the bundled `matrix-rust-sdk`.
*
* Mostly used to identify the exact SDK revision in bug reports and in the user agent.
*/
interface SdkMetadata {
/**
* The full git SHA of the commit the bundled `matrix-rust-sdk` was built from.
*/
val sdkGitSha: String
}
@@ -11,5 +11,11 @@ package io.element.android.libraries.matrix.api
* Factory to create a temporary [TemporaryMatrixClient] for a given home server URL.
*/
interface TemporaryMatrixClientFactory {
/**
* Builds an unauthenticated client pointing at [homeServerUrl], backed by a fresh set of temporary session directories.
* The caller owns the returned client and must close it to delete those directories.
*
* @param homeServerUrl the URL of the homeserver the client will talk to.
*/
suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient>
}
@@ -9,6 +9,14 @@ package io.element.android.libraries.matrix.api.analytics
import io.element.android.libraries.matrix.api.core.SessionId
/**
* Reads the on-disk size of a session's SDK stores, so they can be reported as analytics.
*/
fun interface GetDatabaseSizesUseCase {
/**
* Returns the size of each store of the given session.
*
* @param sessionId the session whose stores are measured.
*/
operator fun invoke(sessionId: SessionId): Result<SdkStoreSizes>
}
@@ -16,23 +16,41 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
/**
* Creates and restores sessions: password login, OAuth login, QR code login, and importing a session from another client.
*
* [setHomeserver] must be called before any login attempt, since it is what tells the SDK which server to authenticate against.
* Every successful login stores the session and notifies the listeners registered through [listenToNewMatrixClients].
*/
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.
*
* @param sessionId the session to restore.
*/
suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient>
/**
* Set the homeserver to use for authentication, and return its details.
*
* @param homeserver the homeserver URL or server name to authenticate against.
*/
suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails>
/**
* Logs in with a password on the homeserver previously set through [setHomeserver].
*
* @param username the user id or its local part.
* @param password the account password.
*/
suspend fun login(username: String, password: String): Result<SessionId>
/**
* Import a session that was created using another client, for instance Element Web.
*
* @param externalSession the tokens and identifiers of the session to import.
*/
suspend fun importCreatedSession(externalSession: ExternalSession): Result<SessionId>
@@ -42,6 +60,9 @@ interface MatrixAuthenticationService {
/**
* Get the OAuth url to display to the user.
*
* @param prompt whether the authentication server should show the sign-in or the account-creation page.
* @param loginHint the user identifier to pre-fill on that page, or `null` to leave it empty.
*/
suspend fun getOAuthUrl(
prompt: OAuthPrompt,
@@ -55,11 +76,17 @@ interface MatrixAuthenticationService {
/**
* Set the existing data about Element Classic session, if any.
*
* @param session the session found in a local Element Classic install, or `null` when there is none to migrate.
*/
fun setElementClassicSession(session: ElementClassicSession?)
/**
* Check if the provided secrets from Element Classic session contain a key backup.
*
* @param userId the user the secrets belong to.
* @param secrets the raw secrets exported from Element Classic.
* @param backupInfo the raw backup description exported alongside them.
*/
fun doSecretsContainBackupKey(
userId: UserId,
@@ -69,11 +96,23 @@ interface MatrixAuthenticationService {
/**
* Attempt to log in using the [callbackUrl] provided by the OAuth page.
*
* @param callbackUrl the redirect URL the authentication server sent the user back to, carrying the authorization code.
*/
suspend fun loginWithOAuth(callbackUrl: String): Result<SessionId>
/**
* Logs in by acting on a QR code shown by an already signed in device, reporting each step of the exchange as it happens.
*
* @param qrCodeData the decoded QR code, obtained from [io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory].
* @param progress called on every step of the flow, so the UI can follow along.
*/
suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result<SessionId>
/** Listen to new Matrix clients being created on authentication. */
/**
* Listen to new Matrix clients being created on authentication.
*
* @param lambda called with each client created by a successful login or session restore.
*/
fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit)
}
@@ -8,6 +8,10 @@
package io.element.android.libraries.matrix.api.auth
/**
* Provides the redirect URL registered for OAuth login, which the authentication server sends the user back to once they have signed in.
*/
interface OAuthRedirectUrlProvider {
/** Returns the redirect URL, built from the app's custom URL scheme so that the system hands the callback back to the app. */
fun provide(): String
}
@@ -8,6 +8,12 @@
package io.element.android.libraries.matrix.api.auth.qrlogin
/**
* The decoded content of a login QR code, as produced by [MatrixQrCodeLoginDataFactory].
*
* The underlying data is opaque to the app: it is passed back to the SDK to perform the login.
*/
interface MatrixQrCodeLoginData {
/** The homeserver advertised by the QR code, or `null` when it does not carry one. */
fun serverName(): String?
}
@@ -8,6 +8,14 @@
package io.element.android.libraries.matrix.api.auth.qrlogin
/**
* Decodes the raw bytes of a scanned QR code into login data the SDK can use.
*/
interface MatrixQrCodeLoginDataFactory {
/**
* Parses the scanned bytes, failing with a [QrCodeDecodeException] when they are not a valid Matrix login QR code.
*
* @param data the raw content read from the QR code.
*/
fun parseQrCodeData(data: ByteArray): Result<MatrixQrCodeLoginData>
}
@@ -8,6 +8,15 @@
package io.element.android.libraries.matrix.api.core
/**
* Reports the progress of a long running transfer, such as a media upload or download.
*/
interface ProgressCallback {
/**
* Called as the transfer advances, possibly many times and from a background thread.
*
* @param current the number of bytes transferred so far.
* @param total the total number of bytes to transfer.
*/
fun onProgress(current: Long, total: Long)
}
@@ -8,6 +8,10 @@
package io.element.android.libraries.matrix.api.core
/**
* A handle on an event sitting in a room's send queue, obtained from a timeline item that failed to send.
*/
fun interface SendHandle {
/** Puts the event back in the send queue for another attempt. */
suspend fun retry(): Result<Unit>
}
@@ -14,13 +14,32 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Gives access to the end-to-end encryption state of the session: key backup, recovery, and the trust of other users' identities.
*/
interface EncryptionService {
/**
* The state of the room key backup on the server.
* Reports [BackupState.WAITING_FOR_SYNC] for as long as the session is not actively syncing, since the real state is unknown until then.
*/
val backupStateStateFlow: StateFlow<BackupState>
/**
* The state of recovery, i.e. whether the secrets needed to restore the keys on a new device are available.
* As with [backupStateStateFlow], it reports a waiting-for-sync state until the session is syncing.
*/
val recoveryStateStateFlow: StateFlow<RecoveryState>
/** Progress of the [enableRecovery] call currently in flight, to be observed while that call is running. */
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
/** Whether this is the only device of the account, which matters because logging out of it can make the room keys unrecoverable. */
val isLastDevice: StateFlow<Boolean>
/** Whether the account has another device this one could be verified against; it stays loading until the first answer arrives. */
val hasDevicesToVerifyAgainst: StateFlow<AsyncData<Boolean>>
/** Enables the room key backup on the server, so that the keys survive losing every device. */
suspend fun enableBackups(): Result<Unit>
/**
@@ -39,12 +58,16 @@ interface EncryptionService {
*/
suspend fun resetRecoveryKey(): Result<String>
/** Disables recovery, which deletes the secrets from server-side storage; the key backup itself is left in place. */
suspend fun disableRecovery(): Result<Unit>
/** Whether a room key backup currently exists on the server, whether or not this device is the one using it. */
suspend fun doesBackupExistOnServer(): Result<Boolean>
/**
* Note: accept both recoveryKey and passphrase.
* Restores the secrets from server-side storage, which unlocks the room key backup for this device.
*
* @param recoveryKey the recovery key, or the passphrase it was derived from; both are accepted.
*/
suspend fun recover(recoveryKey: String): Result<Unit>
@@ -72,6 +95,8 @@ interface EncryptionService {
/**
* Remember this identity, ensuring it does not result in a pin violation.
*
* @param userId the user whose current identity should be pinned.
*/
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
@@ -81,6 +106,8 @@ interface EncryptionService {
* 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.
*
* @param userId the user whose verification should be withdrawn.
*/
suspend fun withdrawVerification(userId: UserId): Result<Unit>
@@ -7,6 +7,9 @@
package io.element.android.libraries.matrix.api.linknewdevice
/**
* Lets the user confirm the two-digit check code displayed by the device being linked, which proves the channel is not being intercepted.
*/
interface CheckCodeSender {
/**
* Validates the given [code]. Returns true if the code is valid, false otherwise.
@@ -7,7 +7,15 @@
package io.element.android.libraries.matrix.api.linknewdevice
/**
* Lets the user confirm or refuse a device linking request once they have approved it in the browser.
*
* Handed out with the step that is waiting for authorisation; exactly one of the two methods is meant to be called.
*/
interface ContinuationMessageSender {
/** Tells the other device that the user refused the linking, which ends the flow. */
suspend fun cancel(): Result<Unit>
/** Tells the other device that the user approved the linking, so the flow can carry on. */
suspend fun confirm(): Result<Unit>
}
@@ -10,8 +10,22 @@ package io.element.android.libraries.matrix.api.linknewdevice
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException
import kotlinx.coroutines.flow.StateFlow
/**
* Drives the linking of a new device that cannot scan a QR code, such as a desktop: this device scans the code the other one displays.
*
* Progress is reported entirely through [linkDesktopStep]; there is one handler per attempt, obtained from
* [io.element.android.libraries.matrix.api.MatrixClient.createLinkDesktopHandler].
*/
interface LinkDesktopHandler {
/** Each step of the flow, from establishing the secure channel to syncing the secrets to the new device. */
val linkDesktopStep: StateFlow<LinkDesktopStep>
/**
* Runs the whole flow from the QR code just scanned, suspending until it finishes or fails.
* A code that cannot be decoded is reported as an invalid-QR-code step rather than as a thrown error.
*
* @param data the raw content read from the QR code shown by the other device.
*/
suspend fun handleScannedQrCode(data: ByteArray)
}
@@ -9,8 +9,17 @@ package io.element.android.libraries.matrix.api.linknewdevice
import kotlinx.coroutines.flow.Flow
/**
* Drives the linking of a new device that is able to scan a QR code, by showing one on this already signed in device.
*
* Progress is reported entirely through [linkMobileStep]; there is one handler per attempt, obtained from
* [io.element.android.libraries.matrix.api.MatrixClient.createLinkMobileHandler].
*/
interface LinkMobileHandler {
/** Each step of the flow, from generating the QR code to syncing the secrets to the new device. */
val linkMobileStep: Flow<LinkMobileStep>
/** Runs the whole flow, suspending until it finishes or fails; the outcome is reported through [linkMobileStep]. */
suspend fun start()
}
@@ -8,6 +8,9 @@
package io.element.android.libraries.matrix.api.media
/**
* Fetches media content from the homeserver, decrypting it on the way when the room is encrypted.
*/
interface MatrixMediaLoader {
/**
* @param source to fetch the content for.
@@ -16,6 +16,7 @@ import java.io.File
* When closed the file will be removed from the disk unless [persist] has been used.
*/
interface MediaFile : Closeable {
/** The absolute path of the file on disk; see [toFile] to get a [File] instead. */
fun path(): String
/**
@@ -10,6 +10,11 @@ package io.element.android.libraries.matrix.api.media
import kotlinx.coroutines.flow.StateFlow
/**
* Reads and writes the account settings that decide whether media previews and invite avatars are shown.
*
* These are account data settings shared across clients, so the local value is only updated once the server has accepted the change.
*/
interface MediaPreviewService {
/**
* Will fetch the media preview config from the server.
@@ -24,11 +29,15 @@ interface MediaPreviewService {
/**
* Set the media preview display policy. This will update the value on the server and update the local value when successful.
*
* @param mediaPreviewValue whether media previews are shown always, never, or only in private rooms.
*/
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.
*
* @param hide true to hide the avatars of users and rooms the user has been invited by.
*/
suspend fun setHideInviteAvatars(hide: Boolean): Result<Unit>
}
@@ -8,6 +8,9 @@
package io.element.android.libraries.matrix.api.mxc
/**
* Helpers to work with Matrix Content (`mxc://`) URIs.
*/
interface MxcTools {
/**
* Sanitizes an mxcUri to be used as a relative file path.
@@ -24,6 +24,9 @@ typealias GetNotificationDataResult = Result<Map<EventId, Result<NotificationDat
interface NotificationService {
/**
* Fetch notifications for the specified event ids in the given rooms.
* Each event is resolved independently, so a partial answer is normal: see [GetNotificationDataResult] for how the two failure levels differ.
*
* @param ids the events to resolve, grouped by the room they belong to.
*/
suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): GetNotificationDataResult
}
@@ -11,28 +11,114 @@ 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
/**
* Reads and writes the push rules of the account, which decide what generates a notification.
*
* Rules are account data shared across clients, so a change made here is visible on the user's other devices.
* Several methods take the encryption and one-to-one nature of a room, because Matrix keeps a separate default rule for each combination.
*/
interface NotificationSettingsService {
/**
* State of the current room notification settings flow ([RoomNotificationSettingsState.Unknown] if not started).
* Emits every time the push rules change, whether from this client or another one, without saying what changed.
* Callers are expected to re-read the values they display; only the most recent signal is buffered.
*/
val notificationSettingsChangeFlow: SharedFlow<Unit>
/**
* Returns the notification settings that apply to one room, which may come from a room specific rule or from the matching default.
*
* @param roomId the room to read the settings of.
* @param isEncrypted whether that room is encrypted, which selects the relevant default rule.
* @param isOneToOne whether that room is a direct message, which selects the relevant default rule.
*/
suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings>
/**
* Returns the default notification mode used by rooms that have no rule of their own.
*
* @param isEncrypted whether to read the default for encrypted rooms.
* @param isOneToOne whether to read the default for direct messages.
*/
suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode>
/**
* Changes the default notification mode for a whole category of rooms.
*
* @param isEncrypted whether to change the default for encrypted rooms.
* @param mode the new default mode.
* @param isDM whether to change the default for direct messages.
*/
suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isDM: Boolean): Result<Unit>
/**
* Sets a room specific notification mode, which from then on overrides the default for that room.
*
* @param roomId the room to change.
* @param mode the new mode for that room.
*/
suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit>
/**
* Removes the room specific rule so the room follows the matching default again.
*
* @param roomId the room to reset.
*/
suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit>
/**
* Mutes a room, which is a room specific rule like any other and is undone by [unmuteRoom].
*
* @param roomId the room to mute.
*/
suspend fun muteRoom(roomId: RoomId): Result<Unit>
/**
* Unmutes a room, restoring either its previous room specific mode or the matching default.
*
* @param roomId the room to unmute.
* @param isEncrypted whether that room is encrypted, which selects the relevant default rule.
* @param isOneToOne whether that room is a direct message, which selects the relevant default rule.
*/
suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<Unit>
/** Whether being mentioned through `@room` generates a notification. */
suspend fun isRoomMentionEnabled(): Result<Boolean>
/**
* Sets whether being mentioned through `@room` generates a notification.
*
* @param enabled true to be notified of `@room` mentions.
*/
suspend fun setRoomMentionEnabled(enabled: Boolean): Result<Unit>
/** Whether incoming calls generate a notification. */
suspend fun isCallEnabled(): Result<Boolean>
/**
* Sets whether incoming calls generate a notification.
*
* @param enabled true to be notified of incoming calls.
*/
suspend fun setCallEnabled(enabled: Boolean): Result<Unit>
/** Whether room invitations generate a notification. */
suspend fun isInviteForMeEnabled(): Result<Boolean>
/**
* Sets whether room invitations generate a notification.
*
* @param enabled true to be notified of invitations.
*/
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
/** The rooms that have an enabled rule of their own rather than following the defaults. */
suspend fun getRoomsWithUserDefinedRules(): Result<List<RoomId>>
/** Whether the homeserver is able to push encrypted events to the device, which decides how much a notification can show before decryption. */
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
/** The push rules as raw JSON, for debugging and bug reports; `null` when the server returned none. */
suspend fun getRawPushRules(): Result<String?>
}
@@ -20,6 +20,9 @@ interface MatrixToConverter {
* - 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
*
* @param uri the uri to convert.
* @return the equivalent matrix.to uri, or `null` when the input is not a permalink this converter recognises.
*/
fun convert(uri: Uri): Uri?
}
@@ -11,8 +11,22 @@ 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
/**
* Builds `matrix.to` permalinks, which are the shareable links to a user or a room; see [PermalinkParser] for the other direction.
*/
interface PermalinkBuilder {
/**
* Builds a permalink to a user, failing with [PermalinkBuilderError.InvalidData] when the id is malformed.
*
* @param userId the user to link to.
*/
fun permalinkForUser(userId: UserId): Result<String>
/**
* Builds a permalink to a room from its alias, failing with [PermalinkBuilderError.InvalidData] when the alias is malformed.
*
* @param roomAlias the room alias to link to.
*/
fun permalinkForRoomAlias(roomAlias: RoomAlias): Result<String>
}
@@ -18,7 +18,10 @@ package io.element.android.libraries.matrix.api.permalink
interface PermalinkParser {
/**
* Turns a uri string to a [PermalinkData].
* A uri that is not a Matrix permalink is reported as a fallback link rather than as an error, so this never throws.
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*
* @param uriString the uri to parse.
*/
fun parse(uriString: String): PermalinkData
}
@@ -8,7 +8,21 @@
package io.element.android.libraries.matrix.api.pusher
/**
* Registers and removes the pusher of this session, i.e. the HTTP gateway the homeserver sends push notifications to.
*/
interface PushersService {
/**
* Registers or updates the pusher; calling it again with the same push key replaces the existing registration.
*
* @param setHttpPusherData the gateway URL, the push key and the other details the homeserver needs to reach the device.
*/
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
/**
* Removes the pusher, so the homeserver stops sending push notifications to this device.
*
* @param unsetHttpPusherData the push key and app id identifying the registration to remove.
*/
suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit>
}
@@ -66,21 +66,30 @@ interface BaseRoom : Closeable {
*/
fun isDm() = roomInfoFlow.value.isDm
/**
* The room this one replaced when it was upgraded, or `null` if this room has no predecessor.
* Errors are swallowed and reported as `null`.
*/
fun predecessorRoom(): PredecessorRoom?
/**
* Try to load the room members and update the membersFlow.
* The first call may serve members from the local cache before the server response arrives; later calls always go to the server.
*/
suspend fun updateMembers()
/**
* Get the members of the room. Note: generally this should not be used, please use
* [membersStateFlow] and [updateMembers] instead.
*
* @param limit the maximum number of members to return.
*/
suspend fun getMembers(limit: Int = 5): Result<List<RoomMember>>
/**
* Will return an updated member or an error.
*
* @param userId the member to fetch.
*/
suspend fun getUpdatedMember(userId: UserId): Result<RoomMember>
@@ -103,6 +112,8 @@ interface BaseRoom : Closeable {
/**
* Gets the role of the user with the provided [userId] in the room.
*
* @param userId the member whose role is requested.
*/
suspend fun userRole(userId: UserId): Result<RoomMember.Role>
@@ -113,11 +124,15 @@ interface BaseRoom : Closeable {
/**
* Gets the display name of the user with the provided [userId] in the room.
*
* @param userId the member whose room specific display name is requested.
*/
suspend fun userDisplayName(userId: UserId): Result<String?>
/**
* Gets the avatar of the user with the provided [userId] in the room.
*
* @param userId the member whose room specific avatar is requested.
*/
suspend fun userAvatarUrl(userId: UserId): Result<String?>
@@ -138,6 +153,8 @@ interface BaseRoom : Closeable {
/**
* Sets the room as favorite or not, based on the [isFavorite] parameter.
*
* @param isFavorite true to mark the room as favorite, false to remove the flag.
*/
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
@@ -176,22 +193,29 @@ interface BaseRoom : Closeable {
suspend fun getRoomVisibility(): Result<RoomVisibility>
/**
* Returns the visibility for this room in the room directory, fetching it from the homeserver if needed.
* Returns whether this room is encrypted, based on the latest encryption state known to the SDK rather than on the cached [RoomInfo].
*/
suspend fun getUpdatedIsEncrypted(): Result<Boolean>
/**
* Store the given `ComposerDraft` in the state store of this room.
*
* @param composerDraft the unsent message content to remember.
* @param threadRoot the thread the draft belongs to, or `null` for the main timeline.
*/
suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result<Unit>
/**
* Retrieve the `ComposerDraft` stored in the state store for this room.
*
* @param threadRoot the thread whose draft is requested, or `null` for the main timeline.
*/
suspend fun loadComposerDraft(threadRoot: ThreadId?): Result<ComposerDraft?>
/**
* Clear the `ComposerDraft` stored in the state store for this room.
*
* @param threadRoot the thread whose draft should be cleared, or `null` for the main timeline.
*/
suspend fun clearComposerDraft(threadRoot: ThreadId?): Result<Unit>
@@ -202,10 +226,27 @@ interface BaseRoom : Closeable {
*/
suspend fun reportRoom(reason: String?): Result<Unit>
/**
* Declines the incoming call advertised by the given notification, letting the other devices of the room know.
*
* @param notificationEventId the id of the call notification event being declined.
*/
suspend fun declineCall(notificationEventId: EventId): Result<Unit>
/**
* Emits the id of each user who declines the call advertised by the given notification.
* Used to stop ringing once the callee has declined from another device.
*
* @param notificationEventId the id of the call notification event to watch.
*/
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId>
/**
* Returns the id of the thread the given event belongs to, fetching the event from the server if it is not cached locally.
*
* @param eventId the event whose thread is requested.
* @return the thread root id, or `null` if the event is not part of a thread.
*/
suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?>
/**
@@ -213,5 +254,6 @@ interface BaseRoom : Closeable {
*/
fun destroy()
/** Same as [destroy], so that a room can be used with `use { }`. */
override fun close() = destroy()
}
@@ -28,11 +28,28 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* A room the current user has joined, adding to [BaseRoom] everything that requires membership: sending events, moderation and room settings.
*
* An instance owns SDK resources, in particular [liveTimeline], so it must be closed once no longer needed.
*/
interface JoinedRoom : BaseRoom {
/**
* A counter incremented every time a synced event is received on [liveTimeline], so callers can refresh data derived from the room.
* It carries no meaning beyond "something changed", and is only updated while it has subscribers.
*/
val syncUpdateFlow: StateFlow<Long>
/** The members currently typing in the room, never including the current user; starts as an empty list. */
val roomTypingMembersFlow: Flow<List<UserId>>
/** Emits when the verification state of other members' identities changes, for instance after a member resets their identity. */
val identityStateChangesFlow: Flow<List<IdentityStateChange>>
/**
* The notification settings that apply to this room.
* Starts as [RoomNotificationSettingsState.Unknown]: [updateRoomNotificationSettings] must be called to populate it.
*/
val roomNotificationSettingsStateFlow: StateFlow<RoomNotificationSettingsState>
/**
@@ -45,6 +62,7 @@ interface JoinedRoom : BaseRoom {
*/
val liveTimeline: Timeline
/** Gives access to the paginated list of threads started in this room. */
val threadsListService: ThreadsListService
/**
@@ -55,6 +73,14 @@ interface JoinedRoom : BaseRoom {
createTimelineParams: CreateTimelineParams,
): Result<Timeline>
/**
* Replaces the content of an event previously sent by the current user, by sending an edit of it.
*
* @param eventId the event to edit.
* @param body the new content, as plain text.
* @param htmlBody the new content as HTML, or `null` when the message has no formatted version.
* @param intentionalMentions the users and rooms this new content deliberately mentions.
*/
suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
/**
@@ -63,12 +89,28 @@ interface JoinedRoom : BaseRoom {
*/
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
/**
* Invites a user to this room.
*
* @param id the user to invite.
*/
suspend fun inviteUserById(id: UserId): Result<Unit>
/**
* Uploads [data] as the new avatar of the room, not of the current user.
*
* @param mimeType the MIME type of the image, for instance `image/jpeg`.
* @param data the raw bytes of the image.
*/
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
/** Removes the avatar of the room. */
suspend fun removeAvatar(): Result<Unit>
/**
* Refreshes [roomNotificationSettingsStateFlow], which moves to a pending state and then to a ready or error state.
* The failure is reported through that flow as well as through the returned [Result].
*/
suspend fun updateRoomNotificationSettings(): Result<Unit>
/**
@@ -83,11 +125,15 @@ interface JoinedRoom : BaseRoom {
/**
* Update the room's visibility in the room directory.
*
* @param roomVisibility whether the room should be listed publicly in the directory.
*/
suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result<Unit>
/**
* Update room history visibility for this room.
* Update room history visibility for this room, i.e. how much of the past a new member is allowed to read.
*
* @param historyVisibility the new history visibility to apply.
*/
suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result<Unit>
@@ -98,6 +144,8 @@ interface JoinedRoom : BaseRoom {
* - `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.
*
* @param roomAlias the alias to publish.
*/
suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result<Boolean>
@@ -108,6 +156,8 @@ interface JoinedRoom : BaseRoom {
* - `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.
*
* @param roomAlias the alias to remove.
*/
suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result<Boolean>
@@ -117,26 +167,74 @@ interface JoinedRoom : BaseRoom {
suspend fun enableEncryption(): Result<Unit>
/**
* Update the join rule for this room.
* Update the join rule for this room, i.e. who is allowed to join it.
*
* @param joinRule the new join rule to apply.
*/
suspend fun updateJoinRule(joinRule: JoinRule): Result<Unit>
/**
* Changes the power level of individual members, which is how roles such as moderator or administrator are granted.
*
* @param changes the new power level to apply to each user.
*/
suspend fun updateUsersRoles(changes: List<UserRoleChange>): Result<Unit>
/**
* Changes the power level required for each action in the room, such as inviting, kicking or changing the room name.
*
* @param roomPowerLevelsValues the new threshold for every action.
*/
suspend fun updatePowerLevels(roomPowerLevelsValues: RoomPowerLevelsValues): Result<Unit>
/** Restores the power levels required for each action to the defaults the room was created with. */
suspend fun resetPowerLevels(): Result<Unit>
/**
* Changes the name of the room.
*
* @param name the new room name.
*/
suspend fun setName(name: String): Result<Unit>
/**
* Changes the topic of the room.
*
* @param topic the new room topic.
*/
suspend fun setTopic(topic: String): Result<Unit>
/**
* Reports an event to the homeserver moderators, optionally ignoring its sender at the same time.
*
* @param eventId the event being reported.
* @param reason the explanation sent to the moderators.
* @param blockUserId the sender to also add to the ignore list, or `null` to only report the event.
*/
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
/**
* Removes a member from the room; they are able to join again unless they are also banned.
*
* @param userId the member to remove.
* @param reason the reason recorded in the membership event, or `null` for none.
*/
suspend fun kickUser(userId: UserId, reason: String? = null): Result<Unit>
/**
* Bans a user from the room, removing them if they are a member and preventing them from joining again.
*
* @param userId the user to ban.
* @param reason the reason recorded in the membership event, or `null` for none.
*/
suspend fun banUser(userId: UserId, reason: String? = null): Result<Unit>
/**
* Lifts the ban on a user, allowing them to join the room again; it does not invite them back.
*
* @param userId the user to unban.
* @param reason the reason recorded in the membership event, or `null` for none.
*/
suspend fun unbanUser(userId: UserId, reason: String? = null): Result<Unit>
/**
@@ -161,6 +259,12 @@ interface JoinedRoom : BaseRoom {
*/
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
/**
* Enables or disables the send queue of this room only; see [io.element.android.libraries.matrix.api.MatrixClient.setAllSendQueuesEnabled]
* for the session-wide equivalent and for why a queue disables itself.
*
* @param enabled whether the send queue of this room should be enabled.
*/
suspend fun setSendQueueEnabled(enabled: Boolean)
/**
@@ -12,7 +12,13 @@ import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
/** A reference to a room either invited, knocked or banned. */
interface NotJoinedRoom : AutoCloseable {
/** What is publicly known about the room: name, topic, avatar, join rule and member count. */
val previewInfo: RoomPreviewInfo
/**
* The local room, available when the user has a membership in it, for instance after being invited.
* It is `null` for a room only discovered through a preview, and closing this instance also closes it.
*/
val localRoom: BaseRoom?
/**
@@ -10,7 +10,21 @@ package io.element.android.libraries.matrix.api.room.alias
import io.element.android.libraries.matrix.api.core.RoomAlias
/**
* Helpers to build and validate room aliases, delegating to the SDK so that the app and the server agree on what is acceptable.
*/
interface RoomAliasHelper {
/**
* Turns a room display name into a usable alias local part, dropping the characters that are not allowed in an alias.
*
* @param name the room display name to derive the alias from.
*/
fun roomAliasNameFromRoomDisplayName(name: String): String
/**
* Whether the given alias is well-formed; this is a format check only and says nothing about the alias being taken.
*
* @param roomAlias the alias to validate.
*/
fun isRoomAliasValid(roomAlias: RoomAlias): Boolean
}
@@ -11,7 +11,18 @@ package io.element.android.libraries.matrix.api.room.join
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
/**
* Joins a room and reports the join to analytics, so that callers do not have to do both.
*/
interface JoinRoom {
/**
* Joins the room, picking the right SDK call depending on whether an id or an alias was given, and captures an analytics event on success.
* A `Forbidden` answer from the server is translated into [Failures.UnauthorizedJoin] so callers can tell it apart from other errors.
*
* @param roomIdOrAlias the id or the alias of the room to join.
* @param serverNames servers to ask about the room, ignored when joining by alias.
* @param trigger what made the user join, recorded in the analytics event.
*/
suspend operator fun invoke(
roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String>,

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