Merge branch 'develop' into fix-6232

This commit is contained in:
Timur Gilfanov
2026-03-05 13:34:09 +04:00
committed by GitHub
199 changed files with 2907 additions and 2420 deletions

View File

@@ -57,6 +57,7 @@ dependencies {
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.features.login.test) testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)

View File

@@ -8,7 +8,6 @@
package io.element.android.appnav package io.element.android.appnav
import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -63,6 +62,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.startchat.api.StartChatEntryPoint import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@@ -307,7 +307,7 @@ class LoggedInFlowNode(
data object RoomDirectory : NavTarget data object RoomDirectory : NavTarget
@Parcelize @Parcelize
data class IncomingShare(val intent: Intent) : NavTarget data class IncomingShare(val shareIntentData: ShareIntentData) : NavTarget
@Parcelize @Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
@@ -570,7 +570,7 @@ class LoggedInFlowNode(
shareEntryPoint.createNode( shareEntryPoint.createNode(
parentNode = this, parentNode = this,
buildContext = buildContext, buildContext = buildContext,
params = ShareEntryPoint.Params(intent = navTarget.intent), params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
callback = object : ShareEntryPoint.Callback { callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) { override fun onDone(roomIds: List<RoomId>) {
// Remove the incoming share screen // Remove the incoming share screen
@@ -649,13 +649,13 @@ class LoggedInFlowNode(
} }
} }
internal suspend fun attachIncomingShare(intent: Intent) { internal suspend fun attachIncomingShare(shareIntentData: ShareIntentData) {
waitForNavTargetAttached { navTarget -> waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home navTarget is NavTarget.Home
} }
attachChild<Node> { attachChild<Node> {
backstack.push( backstack.push(
NavTarget.IncomingShare(intent) NavTarget.IncomingShare(shareIntentData)
) )
} }
} }

View File

@@ -44,6 +44,7 @@ import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
@@ -265,7 +266,7 @@ class RootFlowNode(
@Parcelize data class AccountSelect( @Parcelize data class AccountSelect(
val currentSessionId: SessionId, val currentSessionId: SessionId,
val intent: Intent?, val shareIntentData: ShareIntentData?,
val permalinkData: PermalinkData?, val permalinkData: PermalinkData?,
) : NavTarget ) : NavTarget
@@ -357,8 +358,8 @@ class RootFlowNode(
backstack.pop() backstack.pop()
} }
attachSession(sessionId).apply { attachSession(sessionId).apply {
if (navTarget.intent != null) { if (navTarget.shareIntentData != null) {
attachIncomingShare(navTarget.intent) attachIncomingShare(navTarget.shareIntentData)
} else if (navTarget.permalinkData != null) { } else if (navTarget.permalinkData != null) {
attachPermalinkData(navTarget.permalinkData) attachPermalinkData(navTarget.permalinkData)
} }
@@ -392,7 +393,7 @@ class RootFlowNode(
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params) is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData) is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.intent) is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData)
} }
} }
@@ -423,7 +424,7 @@ class RootFlowNode(
} }
} }
private suspend fun onIncomingShare(intent: Intent) { private suspend fun onIncomingShare(shareIntentData: ShareIntentData) {
// Is there a session already? // Is there a session already?
val latestSessionId = sessionStore.getLatestSessionId() val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) { if (latestSessionId == null) {
@@ -437,13 +438,13 @@ class RootFlowNode(
backstack.push( backstack.push(
NavTarget.AccountSelect( NavTarget.AccountSelect(
currentSessionId = latestSessionId, currentSessionId = latestSessionId,
intent = intent, shareIntentData = shareIntentData,
permalinkData = null, permalinkData = null,
) )
) )
} else { } else {
// Only one account, directly attach the incoming share node. // Only one account, directly attach the incoming share node.
loggedInFlowNode.attachIncomingShare(intent) loggedInFlowNode.attachIncomingShare(shareIntentData)
} }
} }
} }
@@ -467,7 +468,7 @@ class RootFlowNode(
backstack.push( backstack.push(
NavTarget.AccountSelect( NavTarget.AccountSelect(
currentSessionId = latestSessionId, currentSessionId = latestSessionId,
intent = null, shareIntentData = null,
permalinkData = permalinkData, permalinkData = permalinkData,
) )
) )

View File

@@ -12,6 +12,8 @@ import android.content.Intent
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.login.api.LoginIntentResolver import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.ShareIntentHandler
import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.api.DeeplinkParser import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
@@ -25,7 +27,7 @@ sealed interface ResolvedIntent {
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
data class Login(val params: LoginParams) : ResolvedIntent data class Login(val params: LoginParams) : ResolvedIntent
data class IncomingShare(val intent: Intent) : ResolvedIntent data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent
} }
@Inject @Inject
@@ -34,6 +36,7 @@ class IntentResolver(
private val loginIntentResolver: LoginIntentResolver, private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver, private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
private val shareIntentHandler: ShareIntentHandler,
) { ) {
fun resolve(intent: Intent): ResolvedIntent? { fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null if (intent.canBeIgnored()) return null
@@ -62,7 +65,8 @@ class IntentResolver(
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData) if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
if (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) { if (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) {
return ResolvedIntent.IncomingShare(intent) val data = shareIntentHandler.handleIncomingShareIntent(intent) ?: return null
return ResolvedIntent.IncomingShare(data)
} }
// Unknown intent // Unknown intent

View File

@@ -15,6 +15,9 @@ import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginIntentResolver import io.element.android.features.login.test.FakeLoginIntentResolver
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.UriToShare
import io.element.android.features.share.test.FakeShareIntentHandler
import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
@@ -239,26 +242,34 @@ class IntentResolverTest {
@Test @Test
fun `test incoming share simple`() { fun `test incoming share simple`() {
val shareIntentData = ShareIntentData.PlainText("Hello")
val sut = createIntentResolver( val sut = createIntentResolver(
oidcIntentResolverResult = { null }, oidcIntentResolverResult = { null },
onIncomingShareIntent = { shareIntentData },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hello")
} }
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(intent = intent)) assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(shareIntentData))
} }
@Test @Test
fun `test incoming share multiple`() { fun `test incoming share multiple`() {
val fileUri = "content://com.example.app/file1.jpg".toUri()
val shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg")))
val sut = createIntentResolver( val sut = createIntentResolver(
oidcIntentResolverResult = { null }, oidcIntentResolverResult = { null },
onIncomingShareIntent = { shareIntentData },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND_MULTIPLE action = Intent.ACTION_SEND_MULTIPLE
putExtra(Intent.EXTRA_TEXT, "Hello")
data = fileUri
} }
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(intent = intent)) assertThat(result).isEqualTo(ResolvedIntent.IncomingShare(shareIntentData))
} }
@Test @Test
@@ -296,6 +307,7 @@ class IntentResolverTest {
permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
onIncomingShareIntent: (Intent) -> ShareIntentData? = { null },
): IntentResolver { ): IntentResolver {
return IntentResolver( return IntentResolver(
deeplinkParser = { deeplinkParserResult }, deeplinkParser = { deeplinkParserResult },
@@ -308,6 +320,9 @@ class IntentResolverTest {
permalinkParser = FakePermalinkParser( permalinkParser = FakePermalinkParser(
result = permalinkParserResult result = permalinkParserResult
), ),
shareIntentHandler = FakeShareIntentHandler(
onIncomingShareIntent = onIncomingShareIntent,
),
) )
} }
} }

View File

@@ -144,8 +144,8 @@ Prerequisites:
``` ```
You can then build the Rust SDK by running the script You can then build the Rust SDK by running the script
[`tools/sdk/build_rust_sdk.sh`](../tools/sdk/build_rust_sdk.sh) and just answering [`tools/sdk/build-rust-sdk`](../tools/sdk/build-rust-sdk). Type
the questions. `./tools/sdk/build-rust-sdk --help` for help.
This will prompt you for the path to the Rust SDK, then build it and This will prompt you for the path to the Rust SDK, then build it and
`matrix-rust-components-kotlin`, eventually producing an aar file at `matrix-rust-components-kotlin`, eventually producing an aar file at

View File

@@ -189,9 +189,13 @@ class LinkNewDeviceFlowNode(
is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected
is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError
is ErrorType.NotFound -> ErrorScreenType.Expired is ErrorType.NotFound -> ErrorScreenType.Expired
is ErrorType.UnableToCreateDevice -> ErrorScreenType.UnknownError is ErrorType.DeviceNotFound -> ErrorScreenType.UnknownError
is ErrorType.Unknown -> ErrorScreenType.UnknownError is ErrorType.Unknown -> ErrorScreenType.UnknownError
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
is ErrorType.Cancelled -> ErrorScreenType.UnknownError
is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected
is ErrorType.Expired -> ErrorScreenType.Expired
is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.UnknownError
} }
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set, // It is OK to push on backstack, since when user leaves the error screen, a new root will be set,
// or the whole flow will be popped. // or the whole flow will be popped.

View File

@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -149,7 +150,7 @@ class LogoutPresenterTest {
@Test @Test
fun `present - logout then confirm`() = runTest { fun `present - logout then confirm`() = runTest {
val cancelWorkManagerJobsLambda = lambdaRecorder<SessionId, Unit> {} val cancelWorkManagerJobsLambda = lambdaRecorder<SessionId, WorkManagerRequestType?, Unit> { _, _ -> }
val workManagerScheduler = FakeWorkManagerScheduler(cancelLambda = cancelWorkManagerJobsLambda) val workManagerScheduler = FakeWorkManagerScheduler(cancelLambda = cancelWorkManagerJobsLambda)
val presenter = createLogoutPresenter(workManagerScheduler = workManagerScheduler) val presenter = createLogoutPresenter(workManagerScheduler = workManagerScheduler)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@@ -238,7 +239,7 @@ class LogoutPresenterTest {
internal fun createLogoutPresenter( internal fun createLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = {}), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = { _, _ -> }),
): LogoutPresenter = LogoutPresenter( ): LogoutPresenter = LogoutPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
encryptionService = encryptionService, encryptionService = encryptionService,

View File

@@ -115,7 +115,7 @@ private fun ViolationAlert(
}, },
submitText = stringResource(submitTextId), submitText = stringResource(submitTextId),
onSubmitClick = onSubmitClick, onSubmitClick = onSubmitClick,
level = if (isCritical) ComposerAlertLevel.Critical else ComposerAlertLevel.Default, level = if (isCritical) ComposerAlertLevel.Critical else ComposerAlertLevel.Info,
) )
} }

View File

@@ -30,5 +30,5 @@ data class TimelineItemStickerContent(
/* Stickers are supposed to be small images so /* Stickers are supposed to be small images so
we allow using the mediaSource (unless the url is empty) */ we allow using the mediaSource (unless the url is empty) */
val preferredMediaSource = if (mediaSource.url.isEmpty()) thumbnailSource else mediaSource val preferredMediaSource = if (mediaSource.safeUrl.isEmpty()) thumbnailSource else mediaSource
} }

View File

@@ -31,6 +31,7 @@ dependencies {
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.workmanager.api)
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.migration.impl.migrations
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
/**
* Remove existing fetch notifications work manager requests since their format has changed.
*/
@ContributesIntoSet(AppScope::class)
class AppMigration10(
private val sessionStore: SessionStore,
private val workManagerScheduler: WorkManagerScheduler,
) : AppMigration {
override val order: Int = 10
override suspend fun migrate(isFreshInstall: Boolean) {
if (isFreshInstall) return
val sessions = sessionStore.getAllSessions()
for (session in sessions) {
workManagerScheduler.cancel(
sessionId = SessionId(session.userId),
requestType = WorkManagerRequestType.NOTIFICATION_SYNC
)
}
}
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.share.api
/**
* Post-processing to be done once a [ShareIntentData] has been consumed.
*/
fun interface OnSharedData {
operator fun invoke(data: ShareIntentData)
}

View File

@@ -8,7 +8,6 @@
package io.element.android.features.share.api package io.element.android.features.share.api
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@@ -16,7 +15,7 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface ShareEntryPoint : FeatureEntryPoint { interface ShareEntryPoint : FeatureEntryPoint {
data class Params(val intent: Intent) data class Params(val shareIntentData: ShareIntentData)
fun createNode( fun createNode(
parentNode: Node, parentNode: Node,

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.share.api
import android.net.Uri
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Share intent data, mapped from the original [android.content.Intent].
*/
sealed interface ShareIntentData : Parcelable {
/**
* A list of [Uri]s to share and their mime types, with an optional [text] to be used as caption.
*/
@Parcelize
data class Uris(val text: String?, val uris: List<UriToShare>) : ShareIntentData
/**
* A plain text to share.
*/
@Parcelize
data class PlainText(val content: String) : ShareIntentData
}
/**
* A [Uri] coming from an external share intent, with its associated [mimeType].
*/
@Parcelize
data class UriToShare(
val uri: Uri,
val mimeType: String,
) : Parcelable

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.share.api
import android.content.Intent
interface ShareIntentHandler {
/**
* This methods aims to handle incoming share intents and parse its data.
*
* @return the [ShareIntentData] if it could be resolved, or null.
*/
fun handleIncomingShareIntent(
intent: Intent
): ShareIntentData?
}

View File

@@ -44,6 +44,7 @@ dependencies {
api(projects.features.share.api) api(projects.features.share.api)
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.share.impl
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber
import kotlin.collections.forEach
@ContributesBinding(AppScope::class)
class DefaultOnSharedData(
@ApplicationContext private val context: Context,
) : OnSharedData {
override fun invoke(data: ShareIntentData) {
when (data) {
is ShareIntentData.PlainText -> {
// No-op, there is nothing to do for plain text intents.
}
is ShareIntentData.Uris -> {
revokeUriPermissions(data.uris.map { it.uri })
}
}
}
private fun revokeUriPermissions(uris: List<Uri>) {
uris.forEach { uri ->
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.revokeUriPermission(context.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
} catch (e: Exception) {
Timber.w(e, "Unable to revoke Uri permission")
}
}
}
}

View File

@@ -26,7 +26,7 @@ class DefaultShareEntryPoint : ShareEntryPoint {
return parentNode.createNode<ShareNode>( return parentNode.createNode<ShareNode>(
buildContext = buildContext, buildContext = buildContext,
plugins = listOf( plugins = listOf(
ShareNode.Inputs(intent = params.intent), ShareNode.Inputs(shareIntentData = params.shareIntentData),
callback, callback,
) )
) )

View File

@@ -1,6 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -14,10 +13,12 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.ShareIntentHandler
import io.element.android.features.share.api.UriToShare
import io.element.android.libraries.androidutils.compat.queryIntentActivitiesCompat import io.element.android.libraries.androidutils.compat.queryIntentActivitiesCompat
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAny import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAny
@@ -30,37 +31,17 @@ import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber import timber.log.Timber
interface ShareIntentHandler {
data class UriToShare(
val uri: Uri,
val mimeType: String,
)
/**
* This methods aims to handle incoming share intents.
*
* @return true if it can handle the intent data, false otherwise
*/
suspend fun handleIncomingShareIntent(
intent: Intent,
onUris: suspend (List<UriToShare>) -> Boolean,
onPlainText: suspend (String) -> Boolean,
): Boolean
}
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultShareIntentHandler( class DefaultShareIntentHandler(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : ShareIntentHandler { ) : ShareIntentHandler {
override suspend fun handleIncomingShareIntent( override fun handleIncomingShareIntent(
intent: Intent, intent: Intent,
onUris: suspend (List<ShareIntentHandler.UriToShare>) -> Boolean, ): ShareIntentData? {
onPlainText: suspend (String) -> Boolean, val type = intent.resolveType(context) ?: return null
): Boolean {
val type = intent.resolveType(context) ?: return false
val uris = getIncomingUris(intent, type) val uris = getIncomingUris(intent, type)
return when { return when {
uris.isEmpty() && type == MimeTypes.PlainText -> handlePlainText(intent, onPlainText) uris.isEmpty() && type == MimeTypes.PlainText -> handlePlainText(intent)
type.isMimeTypeImage() || type.isMimeTypeImage() ||
type.isMimeTypeVideo() || type.isMimeTypeVideo() ||
type.isMimeTypeAudio() || type.isMimeTypeAudio() ||
@@ -68,20 +49,21 @@ class DefaultShareIntentHandler(
type.isMimeTypeFile() || type.isMimeTypeFile() ||
type.isMimeTypeText() || type.isMimeTypeText() ||
type.isMimeTypeAny() -> { type.isMimeTypeAny() -> {
val result = onUris(uris) ShareIntentData.Uris(
revokeUriPermissions(uris.map { it.uri }) text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()?.takeIf { it.isNotEmpty() },
result uris = uris,
)
} }
else -> false else -> null
} }
} }
private suspend fun handlePlainText(intent: Intent, onPlainText: suspend (String) -> Boolean): Boolean { private fun handlePlainText(intent: Intent): ShareIntentData.PlainText? {
val content = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString() val content = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
return if (content?.isNotEmpty() == true) { return if (content?.isNotEmpty() == true) {
onPlainText(content) ShareIntentData.PlainText(content)
} else { } else {
false null
} }
} }
@@ -89,7 +71,7 @@ class DefaultShareIntentHandler(
* Use this function to retrieve files which are shared from another application or internally * Use this function to retrieve files which are shared from another application or internally
* by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions. * by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions.
*/ */
private fun getIncomingUris(intent: Intent, fallbackMimeType: String): List<ShareIntentHandler.UriToShare> { private fun getIncomingUris(intent: Intent, fallbackMimeType: String): List<UriToShare> {
val uriList = mutableListOf<Uri>() val uriList = mutableListOf<Uri>()
if (intent.action == Intent.ACTION_SEND) { if (intent.action == Intent.ACTION_SEND) {
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java) IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)
@@ -118,24 +100,10 @@ class DefaultShareIntentHandler(
// The value in fallbackMimeType can be wrong, especially if several uris were received // The value in fallbackMimeType can be wrong, especially if several uris were received
// in the same intent (i.e. 'image/*'). We need to check the mime type of each uri. // in the same intent (i.e. 'image/*'). We need to check the mime type of each uri.
val mimeType = context.contentResolver.getType(uri) ?: fallbackMimeType val mimeType = context.contentResolver.getType(uri) ?: fallbackMimeType
ShareIntentHandler.UriToShare( UriToShare(
uri = uri, uri = uri,
mimeType = mimeType, mimeType = mimeType,
) )
} }
} }
private fun revokeUriPermissions(uris: List<Uri>) {
uris.forEach { uri ->
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.revokeUriPermission(context.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
} catch (e: Exception) {
Timber.w(e, "Unable to revoke Uri permission")
}
}
}
} }

View File

@@ -8,7 +8,6 @@
package io.element.android.features.share.impl package io.element.android.features.share.impl
import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -23,6 +22,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
@@ -50,10 +50,10 @@ class ShareNode(
@Parcelize @Parcelize
object NavTarget : Parcelable object NavTarget : Parcelable
data class Inputs(val intent: Intent) : NodeInputs data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.intent) private val presenter = presenterFactory.create(inputs.shareIntentData)
private val callback: ShareEntryPoint.Callback = callback() private val callback: ShareEntryPoint.Callback = callback()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {

View File

@@ -8,13 +8,14 @@
package io.element.android.features.share.impl package io.element.android.features.share.impl
import android.content.Intent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -32,24 +33,24 @@ import kotlin.coroutines.cancellation.CancellationException
@AssistedInject @AssistedInject
class SharePresenter( class SharePresenter(
@Assisted private val intent: Intent, @Assisted private val shareIntentData: ShareIntentData,
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val shareIntentHandler: ShareIntentHandler,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val mediaSenderRoomFactory: MediaSenderRoomFactory, private val mediaSenderRoomFactory: MediaSenderRoomFactory,
private val activeRoomsHolder: ActiveRoomsHolder, private val activeRoomsHolder: ActiveRoomsHolder,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
private val onSharedData: OnSharedData,
) : Presenter<ShareState> { ) : Presenter<ShareState> {
@AssistedFactory @AssistedFactory
fun interface Factory { fun interface Factory {
fun create(intent: Intent): SharePresenter fun create(shareIntentData: ShareIntentData): SharePresenter
} }
private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized) private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List<RoomId>) { fun onRoomSelected(roomIds: List<RoomId>) {
sessionCoroutineScope.share(intent, roomIds) sessionCoroutineScope.share(shareIntentData, roomIds)
} }
@Composable @Composable
@@ -73,13 +74,24 @@ class SharePresenter(
} }
private fun CoroutineScope.share( private fun CoroutineScope.share(
intent: Intent, shareIntentData: ShareIntentData,
roomIds: List<RoomId>, roomIds: List<RoomId>,
) = launch { ) = launch {
suspend { suspend {
val result = shareIntentHandler.handleIncomingShareIntent( val result = when (shareIntentData) {
intent, is ShareIntentData.PlainText -> {
onUris = { filesToShare -> roomIds
.map { roomId ->
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
body = shareIntentData.content,
htmlBody = null,
intentionalMentions = emptyList(),
)?.isSuccess.orFalse()
}
.all { it }
}
is ShareIntentData.Uris -> {
val filesToShare = shareIntentData.uris
if (filesToShare.isEmpty()) { if (filesToShare.isEmpty()) {
false false
} else { } else {
@@ -90,6 +102,7 @@ class SharePresenter(
filesToShare filesToShare
.map { fileToShare -> .map { fileToShare ->
val result = mediaSender.sendMedia( val result = mediaSender.sendMedia(
caption = shareIntentData.text,
uri = fileToShare.uri, uri = fileToShare.uri,
mimeType = fileToShare.mimeType, mimeType = fileToShare.mimeType,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(), mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
@@ -113,19 +126,12 @@ class SharePresenter(
} }
.all { it } .all { it }
} }
},
onPlainText = { text ->
roomIds
.map { roomId ->
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
body = text,
htmlBody = null,
intentionalMentions = emptyList(),
)?.isSuccess.orFalse()
}
.all { it }
} }
) }
// Handle post-processing of shared data
onSharedData(shareIntentData)
if (!result) { if (!result) {
error("Failed to handle incoming share intent") error("Failed to handle incoming share intent")
} }

View File

@@ -8,13 +8,14 @@
package io.element.android.features.share.impl package io.element.android.features.share.impl
import android.content.Intent
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
@@ -44,7 +45,7 @@ class DefaultShareEntryPointTest {
override fun onDone(roomIds: List<RoomId>) = lambdaError() override fun onDone(roomIds: List<RoomId>) = lambdaError()
} }
val params = ShareEntryPoint.Params( val params = ShareEntryPoint.Params(
intent = Intent(), shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
) )
val result = entryPoint.createNode( val result = entryPoint.createNode(
parentNode = parentNode, parentNode = parentNode,
@@ -53,7 +54,7 @@ class DefaultShareEntryPointTest {
callback = callback, callback = callback,
) )
assertThat(result).isInstanceOf(ShareNode::class.java) assertThat(result).isInstanceOf(ShareNode::class.java)
assertThat(result.plugins).contains(ShareNode.Inputs(params.intent)) assertThat(result.plugins).contains(ShareNode.Inputs(params.shareIntentData))
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)
} }
} }

View File

@@ -1,27 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.share.impl
import android.content.Intent
class FakeShareIntentHandler(
private val onIncomingShareIntent: suspend (
Intent,
suspend (List<ShareIntentHandler.UriToShare>) -> Boolean,
suspend (String) -> Boolean,
) -> Boolean = { _, _, _ -> false },
) : ShareIntentHandler {
override suspend fun handleIncomingShareIntent(
intent: Intent,
onUris: suspend (List<ShareIntentHandler.UriToShare>) -> Boolean,
onPlainText: suspend (String) -> Boolean,
): Boolean {
return onIncomingShareIntent(intent, onUris, onPlainText)
}
}

View File

@@ -8,12 +8,14 @@
package io.element.android.features.share.impl package io.element.android.features.share.impl
import android.content.Intent
import android.net.Uri import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.share.api.OnSharedData
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.UriToShare
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@@ -72,8 +74,17 @@ class SharePresenterTest {
@Test @Test
fun `present - on room selected ok`() = runTest { fun `present - on room selected ok`() = runTest {
val joinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
sendMessageLambda = { _, _, _ -> Result.success(Unit) }
},
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, joinedRoom)
}
val presenter = createSharePresenter( val presenter = createSharePresenter(
shareIntentHandler = FakeShareIntentHandler { _, _, _ -> true } matrixClient = matrixClient,
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@@ -100,9 +111,7 @@ class SharePresenterTest {
} }
val presenter = createSharePresenter( val presenter = createSharePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
shareIntentHandler = FakeShareIntentHandler { _, _, onText -> shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
onText(A_MESSAGE)
}
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@@ -131,16 +140,15 @@ class SharePresenterTest {
) )
val presenter = createSharePresenter( val presenter = createSharePresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
shareIntentHandler = FakeShareIntentHandler { _, onFile, _ -> shareIntentData = ShareIntentData.Uris(
onFile( text = A_MESSAGE,
listOf( listOf(
ShareIntentHandler.UriToShare( UriToShare(
uri = Uri.parse("content://image.jpg"), uri = Uri.parse("content://image.jpg"),
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
)
) )
) )
}, ),
mediaSenderRoomFactory = MediaSenderRoomFactory { mediaSender }, mediaSenderRoomFactory = MediaSenderRoomFactory { mediaSender },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@@ -159,20 +167,20 @@ class SharePresenterTest {
} }
internal fun TestScope.createSharePresenter( internal fun TestScope.createSharePresenter(
intent: Intent = Intent(), shareIntentData: ShareIntentData = ShareIntentData.PlainText(A_MESSAGE),
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(), activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { FakeMediaSender() }, mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { FakeMediaSender() },
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
onSharedData: OnSharedData = OnSharedData {},
): SharePresenter { ): SharePresenter {
return SharePresenter( return SharePresenter(
intent = intent, shareIntentData = shareIntentData,
sessionCoroutineScope = this, sessionCoroutineScope = this,
shareIntentHandler = shareIntentHandler,
matrixClient = matrixClient, matrixClient = matrixClient,
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,
mediaSenderRoomFactory = mediaSenderRoomFactory, mediaSenderRoomFactory = mediaSenderRoomFactory,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
onSharedData = onSharedData,
) )
} }

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.share.test"
}
dependencies {
implementation(projects.features.share.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.share.test
import android.content.Intent
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.share.api.ShareIntentHandler
class FakeShareIntentHandler(
private val onIncomingShareIntent: (Intent) -> ShareIntentData? = { null },
) : ShareIntentHandler {
override fun handleIncomingShareIntent(
intent: Intent,
): ShareIntentData? {
return onIncomingShareIntent(intent)
}
}

View File

@@ -51,10 +51,10 @@ telephoto = "0.18.0"
haze = "1.7.2" haze = "1.7.2"
# Dependency analysis # Dependency analysis
dependencyAnalysis = "3.6.0" dependencyAnalysis = "3.6.1"
# DI # DI
metro = "0.11.1" metro = "0.11.2"
# Auto service # Auto service
autoservice = "1.1.1" autoservice = "1.1.1"
@@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should # All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs. # only fix API breaks and may add some TODOs.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.0" matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.1"
# Others # Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@@ -220,7 +220,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.34.3" posthog = "com.posthog:posthog-android:3.34.3"
sentry = "io.sentry:sentry-android:8.33.0" sentry = "io.sentry:sentry-android:8.34.0"
# main branch can be tested replacing the version with main-SNAPSHOT # main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"

File diff suppressed because one or more lines are too long

View File

@@ -155,12 +155,10 @@ private fun getSemanticColors(): ImmutableMap<String, Color> {
"gradientActionStop2" to gradientActionStop2, "gradientActionStop2" to gradientActionStop2,
"gradientActionStop3" to gradientActionStop3, "gradientActionStop3" to gradientActionStop3,
"gradientActionStop4" to gradientActionStop4, "gradientActionStop4" to gradientActionStop4,
"gradientCriticalStop1" to gradientCriticalStop1,
"gradientCriticalStop2" to gradientCriticalStop2,
"gradientInfoStop1" to gradientInfoStop1, "gradientInfoStop1" to gradientInfoStop1,
"gradientInfoStop2" to gradientInfoStop2, "gradientInfoStop2" to gradientInfoStop2,
"gradientInfoStop3" to gradientInfoStop3,
"gradientInfoStop4" to gradientInfoStop4,
"gradientInfoStop5" to gradientInfoStop5,
"gradientInfoStop6" to gradientInfoStop6,
"gradientSubtleStop1" to gradientSubtleStop1, "gradientSubtleStop1" to gradientSubtleStop1,
"gradientSubtleStop2" to gradientSubtleStop2, "gradientSubtleStop2" to gradientSubtleStop2,
"gradientSubtleStop3" to gradientSubtleStop3, "gradientSubtleStop3" to gradientSubtleStop3,

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -25,6 +25,9 @@ object CompoundIcons {
@Composable fun Admin(): ImageVector { @Composable fun Admin(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_admin) return ImageVector.vectorResource(R.drawable.ic_compound_admin)
} }
@Composable fun AdvancedSettings(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_advanced_settings)
}
@Composable fun ArrowDown(): ImageVector { @Composable fun ArrowDown(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_arrow_down) return ImageVector.vectorResource(R.drawable.ic_compound_arrow_down)
} }
@@ -64,6 +67,9 @@ object CompoundIcons {
@Composable fun Bold(): ImageVector { @Composable fun Bold(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_bold) return ImageVector.vectorResource(R.drawable.ic_compound_bold)
} }
@Composable fun Bug(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_bug)
}
@Composable fun Calendar(): ImageVector { @Composable fun Calendar(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_calendar) return ImageVector.vectorResource(R.drawable.ic_compound_calendar)
} }
@@ -460,6 +466,9 @@ object CompoundIcons {
@Composable fun RaisedHandSolid(): ImageVector { @Composable fun RaisedHandSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_raised_hand_solid) return ImageVector.vectorResource(R.drawable.ic_compound_raised_hand_solid)
} }
@Composable fun ReOrder(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_re_order)
}
@Composable fun Reaction(): ImageVector { @Composable fun Reaction(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_reaction) return ImageVector.vectorResource(R.drawable.ic_compound_reaction)
} }
@@ -478,9 +487,18 @@ object CompoundIcons {
@Composable fun Room(): ImageVector { @Composable fun Room(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_room) return ImageVector.vectorResource(R.drawable.ic_compound_room)
} }
@Composable fun RotateLeft(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_rotate_left)
}
@Composable fun RotateRight(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_rotate_right)
}
@Composable fun Search(): ImageVector { @Composable fun Search(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_search) return ImageVector.vectorResource(R.drawable.ic_compound_search)
} }
@Composable fun Section(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_section)
}
@Composable fun Send(): ImageVector { @Composable fun Send(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_send) return ImageVector.vectorResource(R.drawable.ic_compound_send)
} }
@@ -535,6 +553,12 @@ object CompoundIcons {
@Composable fun Sticker(): ImageVector { @Composable fun Sticker(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_sticker) return ImageVector.vectorResource(R.drawable.ic_compound_sticker)
} }
@Composable fun Stop(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_stop)
}
@Composable fun StopSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_stop_solid)
}
@Composable fun Strikethrough(): ImageVector { @Composable fun Strikethrough(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_strikethrough) return ImageVector.vectorResource(R.drawable.ic_compound_strikethrough)
} }
@@ -550,6 +574,9 @@ object CompoundIcons {
@Composable fun TextFormatting(): ImageVector { @Composable fun TextFormatting(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_text_formatting) return ImageVector.vectorResource(R.drawable.ic_compound_text_formatting)
} }
@Composable fun Theme(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_theme)
}
@Composable fun Threads(): ImageVector { @Composable fun Threads(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_threads) return ImageVector.vectorResource(R.drawable.ic_compound_threads)
} }
@@ -559,6 +586,12 @@ object CompoundIcons {
@Composable fun Time(): ImageVector { @Composable fun Time(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_time) return ImageVector.vectorResource(R.drawable.ic_compound_time)
} }
@Composable fun Translate(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_translate)
}
@Composable fun Tree(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_tree)
}
@Composable fun Underline(): ImageVector { @Composable fun Underline(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_underline) return ImageVector.vectorResource(R.drawable.ic_compound_underline)
} }
@@ -607,6 +640,9 @@ object CompoundIcons {
@Composable fun VideoCallOffSolid(): ImageVector { @Composable fun VideoCallOffSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_off_solid) return ImageVector.vectorResource(R.drawable.ic_compound_video_call_off_solid)
} }
@Composable fun VideoCallOutgoingSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_outgoing_solid)
}
@Composable fun VideoCallSolid(): ImageVector { @Composable fun VideoCallSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_solid) return ImageVector.vectorResource(R.drawable.ic_compound_video_call_solid)
} }
@@ -619,6 +655,15 @@ object CompoundIcons {
@Composable fun VoiceCall(): ImageVector { @Composable fun VoiceCall(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call) return ImageVector.vectorResource(R.drawable.ic_compound_voice_call)
} }
@Composable fun VoiceCallDeclinedSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_declined_solid)
}
@Composable fun VoiceCallMissedSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_missed_solid)
}
@Composable fun VoiceCallOutgoingSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_outgoing_solid)
}
@Composable fun VoiceCallSolid(): ImageVector { @Composable fun VoiceCallSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_solid) return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_solid)
} }
@@ -643,9 +688,16 @@ object CompoundIcons {
@Composable fun Windows(): ImageVector { @Composable fun Windows(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_windows) return ImageVector.vectorResource(R.drawable.ic_compound_windows)
} }
@Composable fun ZoomIn(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_zoom_in)
}
@Composable fun ZoomOut(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_zoom_out)
}
val all @Composable get() = persistentListOf<ImageVector>( val all @Composable get() = persistentListOf<ImageVector>(
Admin(), Admin(),
AdvancedSettings(),
ArrowDown(), ArrowDown(),
ArrowLeft(), ArrowLeft(),
ArrowRight(), ArrowRight(),
@@ -659,6 +711,7 @@ object CompoundIcons {
BackspaceSolid(), BackspaceSolid(),
Block(), Block(),
Bold(), Bold(),
Bug(),
Calendar(), Calendar(),
Chart(), Chart(),
Chat(), Chat(),
@@ -791,13 +844,17 @@ object CompoundIcons {
QrCode(), QrCode(),
Quote(), Quote(),
RaisedHandSolid(), RaisedHandSolid(),
ReOrder(),
Reaction(), Reaction(),
ReactionAdd(), ReactionAdd(),
ReactionSolid(), ReactionSolid(),
Reply(), Reply(),
Restart(), Restart(),
Room(), Room(),
RotateLeft(),
RotateRight(),
Search(), Search(),
Section(),
Send(), Send(),
SendSolid(), SendSolid(),
Settings(), Settings(),
@@ -816,14 +873,19 @@ object CompoundIcons {
Spotlight(), Spotlight(),
SpotlightView(), SpotlightView(),
Sticker(), Sticker(),
Stop(),
StopSolid(),
Strikethrough(), Strikethrough(),
SwitchCameraSolid(), SwitchCameraSolid(),
TakePhoto(), TakePhoto(),
TakePhotoSolid(), TakePhotoSolid(),
TextFormatting(), TextFormatting(),
Theme(),
Threads(), Threads(),
ThreadsSolid(), ThreadsSolid(),
Time(), Time(),
Translate(),
Tree(),
Underline(), Underline(),
Unknown(), Unknown(),
UnknownSolid(), UnknownSolid(),
@@ -840,10 +902,14 @@ object CompoundIcons {
VideoCallMissedSolid(), VideoCallMissedSolid(),
VideoCallOff(), VideoCallOff(),
VideoCallOffSolid(), VideoCallOffSolid(),
VideoCallOutgoingSolid(),
VideoCallSolid(), VideoCallSolid(),
VisibilityOff(), VisibilityOff(),
VisibilityOn(), VisibilityOn(),
VoiceCall(), VoiceCall(),
VoiceCallDeclinedSolid(),
VoiceCallMissedSolid(),
VoiceCallOutgoingSolid(),
VoiceCallSolid(), VoiceCallSolid(),
VolumeOff(), VolumeOff(),
VolumeOffSolid(), VolumeOffSolid(),
@@ -852,10 +918,13 @@ object CompoundIcons {
Warning(), Warning(),
WebBrowser(), WebBrowser(),
Windows(), Windows(),
ZoomIn(),
ZoomOut(),
) )
val allResIds get() = persistentListOf( val allResIds get() = persistentListOf(
R.drawable.ic_compound_admin, R.drawable.ic_compound_admin,
R.drawable.ic_compound_advanced_settings,
R.drawable.ic_compound_arrow_down, R.drawable.ic_compound_arrow_down,
R.drawable.ic_compound_arrow_left, R.drawable.ic_compound_arrow_left,
R.drawable.ic_compound_arrow_right, R.drawable.ic_compound_arrow_right,
@@ -869,6 +938,7 @@ object CompoundIcons {
R.drawable.ic_compound_backspace_solid, R.drawable.ic_compound_backspace_solid,
R.drawable.ic_compound_block, R.drawable.ic_compound_block,
R.drawable.ic_compound_bold, R.drawable.ic_compound_bold,
R.drawable.ic_compound_bug,
R.drawable.ic_compound_calendar, R.drawable.ic_compound_calendar,
R.drawable.ic_compound_chart, R.drawable.ic_compound_chart,
R.drawable.ic_compound_chat, R.drawable.ic_compound_chat,
@@ -1001,13 +1071,17 @@ object CompoundIcons {
R.drawable.ic_compound_qr_code, R.drawable.ic_compound_qr_code,
R.drawable.ic_compound_quote, R.drawable.ic_compound_quote,
R.drawable.ic_compound_raised_hand_solid, R.drawable.ic_compound_raised_hand_solid,
R.drawable.ic_compound_re_order,
R.drawable.ic_compound_reaction, R.drawable.ic_compound_reaction,
R.drawable.ic_compound_reaction_add, R.drawable.ic_compound_reaction_add,
R.drawable.ic_compound_reaction_solid, R.drawable.ic_compound_reaction_solid,
R.drawable.ic_compound_reply, R.drawable.ic_compound_reply,
R.drawable.ic_compound_restart, R.drawable.ic_compound_restart,
R.drawable.ic_compound_room, R.drawable.ic_compound_room,
R.drawable.ic_compound_rotate_left,
R.drawable.ic_compound_rotate_right,
R.drawable.ic_compound_search, R.drawable.ic_compound_search,
R.drawable.ic_compound_section,
R.drawable.ic_compound_send, R.drawable.ic_compound_send,
R.drawable.ic_compound_send_solid, R.drawable.ic_compound_send_solid,
R.drawable.ic_compound_settings, R.drawable.ic_compound_settings,
@@ -1026,14 +1100,19 @@ object CompoundIcons {
R.drawable.ic_compound_spotlight, R.drawable.ic_compound_spotlight,
R.drawable.ic_compound_spotlight_view, R.drawable.ic_compound_spotlight_view,
R.drawable.ic_compound_sticker, R.drawable.ic_compound_sticker,
R.drawable.ic_compound_stop,
R.drawable.ic_compound_stop_solid,
R.drawable.ic_compound_strikethrough, R.drawable.ic_compound_strikethrough,
R.drawable.ic_compound_switch_camera_solid, R.drawable.ic_compound_switch_camera_solid,
R.drawable.ic_compound_take_photo, R.drawable.ic_compound_take_photo,
R.drawable.ic_compound_take_photo_solid, R.drawable.ic_compound_take_photo_solid,
R.drawable.ic_compound_text_formatting, R.drawable.ic_compound_text_formatting,
R.drawable.ic_compound_theme,
R.drawable.ic_compound_threads, R.drawable.ic_compound_threads,
R.drawable.ic_compound_threads_solid, R.drawable.ic_compound_threads_solid,
R.drawable.ic_compound_time, R.drawable.ic_compound_time,
R.drawable.ic_compound_translate,
R.drawable.ic_compound_tree,
R.drawable.ic_compound_underline, R.drawable.ic_compound_underline,
R.drawable.ic_compound_unknown, R.drawable.ic_compound_unknown,
R.drawable.ic_compound_unknown_solid, R.drawable.ic_compound_unknown_solid,
@@ -1050,10 +1129,14 @@ object CompoundIcons {
R.drawable.ic_compound_video_call_missed_solid, R.drawable.ic_compound_video_call_missed_solid,
R.drawable.ic_compound_video_call_off, R.drawable.ic_compound_video_call_off,
R.drawable.ic_compound_video_call_off_solid, R.drawable.ic_compound_video_call_off_solid,
R.drawable.ic_compound_video_call_outgoing_solid,
R.drawable.ic_compound_video_call_solid, R.drawable.ic_compound_video_call_solid,
R.drawable.ic_compound_visibility_off, R.drawable.ic_compound_visibility_off,
R.drawable.ic_compound_visibility_on, R.drawable.ic_compound_visibility_on,
R.drawable.ic_compound_voice_call, R.drawable.ic_compound_voice_call,
R.drawable.ic_compound_voice_call_declined_solid,
R.drawable.ic_compound_voice_call_missed_solid,
R.drawable.ic_compound_voice_call_outgoing_solid,
R.drawable.ic_compound_voice_call_solid, R.drawable.ic_compound_voice_call_solid,
R.drawable.ic_compound_volume_off, R.drawable.ic_compound_volume_off,
R.drawable.ic_compound_volume_off_solid, R.drawable.ic_compound_volume_off_solid,
@@ -1062,5 +1145,7 @@ object CompoundIcons {
R.drawable.ic_compound_warning, R.drawable.ic_compound_warning,
R.drawable.ic_compound_web_browser, R.drawable.ic_compound_web_browser,
R.drawable.ic_compound_windows, R.drawable.ic_compound_windows,
R.drawable.ic_compound_zoom_in,
R.drawable.ic_compound_zoom_out,
) )
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -121,18 +121,14 @@ data class SemanticColors(
val gradientActionStop3: Color, val gradientActionStop3: Color,
/** Background gradient stop for super and send buttons */ /** Background gradient stop for super and send buttons */
val gradientActionStop4: Color, val gradientActionStop4: Color,
/** Subtle background gradient stop for critical */
val gradientCriticalStop1: Color,
/** Subtle background gradient stop for critical */
val gradientCriticalStop2: Color,
/** Subtle background gradient stop for info */ /** Subtle background gradient stop for info */
val gradientInfoStop1: Color, val gradientInfoStop1: Color,
/** Subtle background gradient stop for info */ /** Subtle background gradient stop for info */
val gradientInfoStop2: Color, val gradientInfoStop2: Color,
/** Subtle background gradient stop for info */
val gradientInfoStop3: Color,
/** Subtle background gradient stop for info */
val gradientInfoStop4: Color,
/** Subtle background gradient stop for info */
val gradientInfoStop5: Color,
/** Subtle background gradient stop for info */
val gradientInfoStop6: Color,
/** Subtle background gradient stop for message highlight and bloom */ /** Subtle background gradient stop for message highlight and bloom */
val gradientSubtleStop1: Color, val gradientSubtleStop1: Color,
/** Subtle background gradient stop for message highlight and bloom */ /** Subtle background gradient stop for message highlight and bloom */

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsDark = SemanticColors(
gradientActionStop2 = DarkColorTokens.colorGreen900, gradientActionStop2 = DarkColorTokens.colorGreen900,
gradientActionStop3 = DarkColorTokens.colorGreen700, gradientActionStop3 = DarkColorTokens.colorGreen700,
gradientActionStop4 = DarkColorTokens.colorGreen500, gradientActionStop4 = DarkColorTokens.colorGreen500,
gradientInfoStop1 = DarkColorTokens.colorAlphaBlue500, gradientCriticalStop1 = DarkColorTokens.colorRed200,
gradientInfoStop2 = DarkColorTokens.colorAlphaBlue400, gradientCriticalStop2 = DarkColorTokens.colorThemeBg,
gradientInfoStop3 = DarkColorTokens.colorAlphaBlue300, gradientInfoStop1 = DarkColorTokens.colorBlue200,
gradientInfoStop4 = DarkColorTokens.colorAlphaBlue200, gradientInfoStop2 = DarkColorTokens.colorThemeBg,
gradientInfoStop5 = DarkColorTokens.colorAlphaBlue100,
gradientInfoStop6 = DarkColorTokens.colorTransparent,
gradientSubtleStop1 = DarkColorTokens.colorAlphaGreen500, gradientSubtleStop1 = DarkColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = DarkColorTokens.colorAlphaGreen400, gradientSubtleStop2 = DarkColorTokens.colorAlphaGreen400,
gradientSubtleStop3 = DarkColorTokens.colorAlphaGreen300, gradientSubtleStop3 = DarkColorTokens.colorAlphaGreen300,

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsHcDark = SemanticColors(
gradientActionStop2 = DarkHcColorTokens.colorGreen900, gradientActionStop2 = DarkHcColorTokens.colorGreen900,
gradientActionStop3 = DarkHcColorTokens.colorGreen700, gradientActionStop3 = DarkHcColorTokens.colorGreen700,
gradientActionStop4 = DarkHcColorTokens.colorGreen500, gradientActionStop4 = DarkHcColorTokens.colorGreen500,
gradientInfoStop1 = DarkHcColorTokens.colorAlphaBlue500, gradientCriticalStop1 = DarkHcColorTokens.colorRed200,
gradientInfoStop2 = DarkHcColorTokens.colorAlphaBlue400, gradientCriticalStop2 = DarkHcColorTokens.colorThemeBg,
gradientInfoStop3 = DarkHcColorTokens.colorAlphaBlue300, gradientInfoStop1 = DarkHcColorTokens.colorBlue200,
gradientInfoStop4 = DarkHcColorTokens.colorAlphaBlue200, gradientInfoStop2 = DarkHcColorTokens.colorThemeBg,
gradientInfoStop5 = DarkHcColorTokens.colorAlphaBlue100,
gradientInfoStop6 = DarkHcColorTokens.colorTransparent,
gradientSubtleStop1 = DarkHcColorTokens.colorAlphaGreen500, gradientSubtleStop1 = DarkHcColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = DarkHcColorTokens.colorAlphaGreen400, gradientSubtleStop2 = DarkHcColorTokens.colorAlphaGreen400,
gradientSubtleStop3 = DarkHcColorTokens.colorAlphaGreen300, gradientSubtleStop3 = DarkHcColorTokens.colorAlphaGreen300,

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsLight = SemanticColors(
gradientActionStop2 = LightColorTokens.colorGreen700, gradientActionStop2 = LightColorTokens.colorGreen700,
gradientActionStop3 = LightColorTokens.colorGreen900, gradientActionStop3 = LightColorTokens.colorGreen900,
gradientActionStop4 = LightColorTokens.colorGreen1100, gradientActionStop4 = LightColorTokens.colorGreen1100,
gradientInfoStop1 = LightColorTokens.colorAlphaBlue500, gradientCriticalStop1 = LightColorTokens.colorRed200,
gradientInfoStop2 = LightColorTokens.colorAlphaBlue400, gradientCriticalStop2 = LightColorTokens.colorThemeBg,
gradientInfoStop3 = LightColorTokens.colorAlphaBlue300, gradientInfoStop1 = LightColorTokens.colorBlue200,
gradientInfoStop4 = LightColorTokens.colorAlphaBlue200, gradientInfoStop2 = LightColorTokens.colorThemeBg,
gradientInfoStop5 = LightColorTokens.colorAlphaBlue100,
gradientInfoStop6 = LightColorTokens.colorTransparent,
gradientSubtleStop1 = LightColorTokens.colorAlphaGreen500, gradientSubtleStop1 = LightColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = LightColorTokens.colorAlphaGreen400, gradientSubtleStop2 = LightColorTokens.colorAlphaGreen400,
gradientSubtleStop3 = LightColorTokens.colorAlphaGreen300, gradientSubtleStop3 = LightColorTokens.colorAlphaGreen300,

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsHcLight = SemanticColors(
gradientActionStop2 = LightHcColorTokens.colorGreen700, gradientActionStop2 = LightHcColorTokens.colorGreen700,
gradientActionStop3 = LightHcColorTokens.colorGreen900, gradientActionStop3 = LightHcColorTokens.colorGreen900,
gradientActionStop4 = LightHcColorTokens.colorGreen1100, gradientActionStop4 = LightHcColorTokens.colorGreen1100,
gradientInfoStop1 = LightHcColorTokens.colorAlphaBlue500, gradientCriticalStop1 = LightHcColorTokens.colorRed200,
gradientInfoStop2 = LightHcColorTokens.colorAlphaBlue400, gradientCriticalStop2 = LightHcColorTokens.colorThemeBg,
gradientInfoStop3 = LightHcColorTokens.colorAlphaBlue300, gradientInfoStop1 = LightHcColorTokens.colorBlue200,
gradientInfoStop4 = LightHcColorTokens.colorAlphaBlue200, gradientInfoStop2 = LightHcColorTokens.colorThemeBg,
gradientInfoStop5 = LightHcColorTokens.colorAlphaBlue100,
gradientInfoStop6 = LightHcColorTokens.colorTransparent,
gradientSubtleStop1 = LightHcColorTokens.colorAlphaGreen500, gradientSubtleStop1 = LightHcColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = LightHcColorTokens.colorAlphaGreen400, gradientSubtleStop2 = LightHcColorTokens.colorAlphaGreen400,
gradientSubtleStop3 = LightHcColorTokens.colorAlphaGreen300, gradientSubtleStop3 = LightHcColorTokens.colorAlphaGreen300,

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2m0,18c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
<path
android:pathData="M13.49,11.38c0.43,-1.22 0.17,-2.64 -0.81,-3.62a3.47,3.47 0,0 0,-4.1 -0.59l2.35,2.35 -1.41,1.41 -2.35,-2.35c-0.71,1.32 -0.52,2.99 0.59,4.1 0.98,0.98 2.4,1.24 3.62,0.81l3.41,3.41c0.2,0.2 0.51,0.2 0.71,0l1.4,-1.4c0.2,-0.2 0.2,-0.51 0,-0.71z"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
</vector>

View File

@@ -4,11 +4,11 @@
android:autoMirrored="true" android:autoMirrored="true"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<group> <path
<clip-path android:pathData="M15.043,8.457a1,1 0,0 1,1.414 1.414l-2.043,2.043 2.129,2.129a1,1 0,1 1,-1.414 1.414l-2.13,-2.129 -2.127,2.129a1,1 0,0 1,-1.415 -1.414l2.129,-2.129 -2.043,-2.043a1,1 0,0 1,1.414 -1.414L13,10.5z"
android:pathData="M0,0h24v24H0z"/> android:fillColor="#FF000000"/>
<path <path
android:pathData="M21.167,3.75L7.417,3.75c-0.633,0 -1.128,0.32 -1.458,0.807L1,12l4.96,7.434c0.33,0.486 0.824,0.816 1.457,0.816h13.75A1.84,1.84 0,0 0,23 18.417L23,5.583a1.84,1.84 0,0 0,-1.833 -1.833m0,14.667L7.48,18.417L3.2,12l4.272,-6.417h13.695zM10.542,16.583 L13.833,13.293 17.124,16.583 18.417,15.291L15.126,12l3.29,-3.29 -1.292,-1.293 -3.29,3.29 -3.291,-3.29L9.25,8.709 12.54,12l-3.29,3.29z" android:pathData="M20,4a2,2 0,0 1,2 2v12a2,2 0,0 1,-2 2H7.28a2,2 0,0 1,-1.655 -0.877l-4.072,-6a2,2 0,0 1,0 -2.246l4.072,-6A2,2 0,0 1,7.28 4zM3.208,12l4.072,6H20V6H7.28z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000"
</group> android:fillType="evenOdd"/>
</vector> </vector>

View File

@@ -5,6 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:pathData="M21.15,4H7.283c-0.638,0 -1.137,0.311 -1.47,0.782l-4.66,6.73a0.87,0.87 0,0 0,0 0.986l4.66,6.72c0.333,0.462 0.832,0.782 1.47,0.782h13.869C22.168,20 23,19.2 23,18.222V5.778C23,4.8 22.168,4 21.15,4m-3.42,11.822a0.947,0.947 0,0 1,-1.304 0l-2.672,-2.569 -2.672,2.57a0.947,0.947 0,0 1,-1.303 0,0.86 0.86,0 0,1 0,-1.254L12.45,12 9.779,9.431a0.86,0.86 0,0 1,0 -1.253,0.947 0.947,0 0,1 1.303,0l2.672,2.569 2.672,-2.57a0.947,0.947 0,0 1,1.304 0c0.36,0.347 0.36,0.907 0,1.254L15.058,12l2.672,2.569a0.877,0.877 0,0 1,0 1.253" android:pathData="M20,4a2,2 0,0 1,2 2v12a2,2 0,0 1,-2 2L7.33,20a2,2 0,0 1,-1.673 -0.902l-3.937,-6a2,2 0,0 1,0 -2.196l3.937,-6A2,2 0,0 1,7.33 4zM16.457,8.457a1,1 0,0 0,-1.414 0L13,10.5l-2.043,-2.043a1,1 0,0 0,-1.414 1.414l2.043,2.043 -2.129,2.129a1,1 0,0 0,1.414 1.414l2.13,-2.129 2.128,2.129a1,1 0,0 0,1.414 -1.414l-2.129,-2.129 2.043,-2.043a1,1 0,0 0,0 -1.414"
android:fillColor="#FF000000"/> android:fillColor="#FF000000"
android:fillType="evenOdd"/>
</vector> </vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,8h-1.81a6,6 0,0 0,-1.82 -1.96l0.93,-0.93a0.996,0.996 0,1 0,-1.41 -1.41l-1.47,1.47C12.96,5.06 12.49,5 12,5s-0.96,0.06 -1.41,0.17L9.11,3.7A0.996,0.996 0,1 0,7.7 5.11l0.92,0.93C7.88,6.55 7.26,7.22 6.81,8H5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1.09c-0.05,0.33 -0.09,0.66 -0.09,1v1H5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1v1c0,0.34 0.04,0.67 0.09,1H5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-1.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h1c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-1v-1c0,-0.34 -0.04,-0.67 -0.09,-1H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1m-6,8h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1m0,-4h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -4,7 +4,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:pathData="M16.099,2.4a4.1,4.1 0,0 1,-1.057 3.074c-0.747,0.862 -1.878,1.358 -3.07,1.347 -0.075,-1.081 0.315,-2.146 1.085,-2.96 0.78,-0.825 1.866,-1.346 3.042,-1.461m3.767,6.54c-1.37,0.783 -2.213,2.163 -2.234,3.657 0.002,1.69 1.092,3.215 2.768,3.873a9.4,9.4 0,0 1,-1.44 2.723c-0.848,1.178 -1.737,2.329 -3.149,2.35 -0.67,0.015 -1.124,-0.165 -1.596,-0.351 -0.493,-0.195 -1.006,-0.398 -1.809,-0.398 -0.851,0 -1.388,0.21 -1.905,0.412 -0.447,0.174 -0.88,0.343 -1.49,0.367 -1.343,0.046 -2.37,-1.258 -3.25,-2.425 -1.756,-2.383 -3.124,-6.716 -1.29,-9.664 0.861,-1.437 2.471,-2.349 4.241,-2.402 0.763,-0.015 1.494,0.258 2.136,0.497 0.49,0.183 0.928,0.347 1.286,0.347 0.315,0 0.74,-0.157 1.237,-0.34 0.78,-0.288 1.737,-0.64 2.71,-0.545 1.514,0.044 2.917,0.748 3.785,1.9" android:pathData="M16.099,2.4a4.1,4.1 0,0 1,-1.057 3.073c-0.747,0.863 -1.878,1.36 -3.07,1.348 -0.075,-1.081 0.315,-2.146 1.085,-2.96 0.78,-0.825 1.866,-1.346 3.042,-1.461m3.767,6.54c-1.37,0.783 -2.214,2.163 -2.234,3.657 0.002,1.69 1.092,3.215 2.768,3.873a9.4,9.4 0,0 1,-1.44 2.723c-0.848,1.178 -1.737,2.329 -3.149,2.35 -0.671,0.015 -1.124,-0.165 -1.596,-0.351 -0.493,-0.195 -1.006,-0.398 -1.809,-0.398 -0.852,0 -1.388,0.21 -1.905,0.412 -0.447,0.174 -0.88,0.343 -1.49,0.367 -1.343,0.046 -2.37,-1.258 -3.25,-2.425 -1.756,-2.383 -3.124,-6.716 -1.29,-9.664 0.86,-1.437 2.471,-2.349 4.241,-2.402 0.763,-0.015 1.494,0.258 2.135,0.497 0.49,0.183 0.929,0.347 1.287,0.347 0.315,0 0.74,-0.157 1.237,-0.34 0.78,-0.288 1.737,-0.64 2.71,-0.545 1.514,0.044 2.917,0.748 3.785,1.9"
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:fillType="evenOdd"/> android:fillType="evenOdd"/>
</vector> </vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m6,0c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,-6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,-6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6.56,7.98C6.1,7.52 5.31,7.6 5,8.17c-0.28,0.51 -0.5,1.03 -0.67,1.58 -0.19,0.63 0.31,1.25 0.96,1.25h0.01c0.43,0 0.82,-0.28 0.94,-0.7q0.18,-0.6 0.48,-1.17c0.22,-0.37 0.15,-0.84 -0.16,-1.15M5.31,13h-0.02c-0.65,0 -1.15,0.62 -0.96,1.25 0.16,0.54 0.38,1.07 0.66,1.58 0.31,0.57 1.11,0.66 1.57,0.2 0.3,-0.31 0.38,-0.77 0.17,-1.15 -0.2,-0.37 -0.36,-0.76 -0.48,-1.16a0.97,0.97 0,0 0,-0.94 -0.72m2.85,6.02q0.765,0.42 1.59,0.66c0.62,0.18 1.24,-0.32 1.24,-0.96v-0.03c0,-0.43 -0.28,-0.82 -0.7,-0.94 -0.4,-0.12 -0.78,-0.28 -1.15,-0.48a0.97,0.97 0,0 0,-1.16 0.17l-0.03,0.03c-0.45,0.45 -0.36,1.24 0.21,1.55M13,4.07v-0.66c0,-0.89 -1.08,-1.34 -1.71,-0.71L9.17,4.83c-0.4,0.4 -0.4,1.04 0,1.43l2.13,2.08c0.63,0.62 1.7,0.17 1.7,-0.72V6.09c2.84,0.48 5,2.94 5,5.91 0,2.73 -1.82,5.02 -4.32,5.75a0.97,0.97 0,0 0,-0.68 0.94v0.02c0,0.65 0.61,1.14 1.23,0.96A7.976,7.976 0,0 0,20 12c0,-4.08 -3.05,-7.44 -7,-7.93"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24H0z"/>
<path
android:pathData="M14.83,4.83 L12.7,2.7c-0.62,-0.62 -1.7,-0.18 -1.7,0.71v0.66C7.06,4.56 4,7.92 4,12c0,3.64 2.43,6.71 5.77,7.68 0.62,0.18 1.23,-0.32 1.23,-0.96v-0.03a0.97,0.97 0,0 0,-0.68 -0.94A5.98,5.98 0,0 1,6 12c0,-2.97 2.16,-5.43 5,-5.91v1.53c0,0.89 1.07,1.33 1.7,0.71l2.13,-2.08a0.99,0.99 0,0 0,0 -1.42m4.84,4.93q-0.24,-0.825 -0.66,-1.59c-0.31,-0.57 -1.1,-0.66 -1.56,-0.2l-0.01,0.01c-0.31,0.31 -0.38,0.78 -0.17,1.16 0.2,0.37 0.36,0.76 0.48,1.16 0.12,0.42 0.51,0.7 0.94,0.7h0.02c0.65,0 1.15,-0.62 0.96,-1.24M13,18.68v0.02c0,0.65 0.62,1.14 1.24,0.96q0.825,-0.24 1.59,-0.66c0.57,-0.31 0.66,-1.1 0.2,-1.56l-0.02,-0.02a0.97,0.97 0,0 0,-1.16 -0.17c-0.37,0.21 -0.76,0.37 -1.16,0.49 -0.41,0.12 -0.69,0.51 -0.69,0.94m4.44,-2.65c0.46,0.46 1.25,0.37 1.56,-0.2 0.28,-0.51 0.5,-1.04 0.67,-1.59 0.18,-0.62 -0.31,-1.24 -0.96,-1.24h-0.02c-0.44,0 -0.82,0.28 -0.94,0.7q-0.18,0.6 -0.48,1.17c-0.21,0.38 -0.13,0.86 0.17,1.16"
android:fillColor="#FF000000"/>
</group>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16,7a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,15 8q0,0.424 0.287,0.713Q15.576,9 16,9t0.712,-0.287A0.97,0.97 0,0 0,17 8a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,16 7m0,4a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,15 12q0,0.424 0.287,0.713 0.288,0.287 0.713,0.287 0.424,0 0.712,-0.287A0.97,0.97 0,0 0,17 12a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,16 11m0,4a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,15 16q0,0.424 0.287,0.712 0.288,0.288 0.713,0.288 0.424,0 0.712,-0.288A0.97,0.97 0,0 0,17 16a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,16 15m-4,-8L8,7a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,7 8q0,0.424 0.287,0.713Q7.576,9 8,9h4q0.424,0 0.713,-0.287A0.97,0.97 0,0 0,13 8a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,12 7m0,4L8,11a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,7 12q0,0.424 0.287,0.713Q7.576,13 8,13h4q0.424,0 0.713,-0.287A0.97,0.97 0,0 0,13 12a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,12 11m0,4L8,15a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,7 16q0,0.424 0.287,0.712Q7.576,17 8,17h4q0.424,0 0.713,-0.288A0.97,0.97 0,0 0,13 16a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,12 15m7,-12q0.824,0 1.413,0.587Q21,4.176 21,5v14q0,0.824 -0.587,1.413A1.93,1.93 0,0 1,19 21L5,21q-0.824,0 -1.412,-0.587A1.93,1.93 0,0 1,3 19L3,5q0,-0.824 0.587,-1.412A1.93,1.93 0,0 1,5 3zM19,5L5,5v14h14z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16,18v2L8,20v-2zM18,16L18,8a2,2 0,0 0,-2 -2L8,6a2,2 0,0 0,-2 2v8a2,2 0,0 0,2 2v2a4,4 0,0 1,-4 -4L4,8a4,4 0,0 1,4 -4h8a4,4 0,0 1,4 4v8a4,4 0,0 1,-4 4v-2a2,2 0,0 0,2 -2"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,8a4,4 0,0 1,4 -4h8a4,4 0,0 1,4 4v8a4,4 0,0 1,-4 4H8a4,4 0,0 1,-4 -4z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,22c5.52,0 10,-4.48 10,-10S17.52,2 12,2 2,6.48 2,12s4.48,10 10,10m1,-17.93c3.94,0.49 7,3.85 7,7.93s-3.05,7.44 -7,7.93z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13,2a2,2 0,0 1,2 2v4h6a2,2 0,0 1,2 2v12.586c0,0.89 -1.077,1.337 -1.707,0.707L19,21h-8a2,2 0,0 1,-2 -2v-4L5,15l-2.293,2.293c-0.63,0.63 -1.707,0.184 -1.707,-0.707L1,4a2,2 0,0 1,2 -2zM15.5,12.125L12,12.125v1.25h4.37c-0.031,0.73 -0.325,1.457 -0.871,2.151a4.4,4.4 0,0 1,-0.613 -1.026h-1.33c0.202,0.69 0.57,1.335 1.067,1.932 -0.524,0.448 -1.162,0.873 -1.912,1.263l0.578,1.11a11.3,11.3 0,0 0,2.21 -1.483c0.633,0.553 1.382,1.05 2.212,1.483l0.578,-1.11c-0.75,-0.39 -1.388,-0.815 -1.912,-1.263 0.758,-0.912 1.213,-1.939 1.245,-3.057L20,13.375v-1.25h-3.25L16.75,10.25L15.5,10.25zM3,14.172l0.586,-0.586A2,2 0,0 1,5 13h4v-2.47L6.96,10.53L6.563,12L5,12l2.031,-7L8.97,5l0.96,3.312A2,2 0,0 1,11 8h2L13,4L3,4zM7.306,9.245h1.386l-0.67,-2.481h-0.047z"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.01,5v1H11c0.333,0 1,0 1.5,0.5S13,7.667 13,8v7.01c0,0.54 0.45,0.99 0.99,0.99H15v-1a2,2 0,0 1,2 -2h3a2,2 0,0 1,2 2v4a2,2 0,0 1,-2 2h-3a2,2 0,0 1,-2 -2v-1h-1.01C12.34,18 11,16.66 11,15.01V9c0,-1 0,-1 -1,-1H9v1a2,2 0,0 1,-2 2H4a2,2 0,0 1,-2 -2V5c0,-1.1 0.9,-2 2,-2h3.01a2,2 0,0 1,2 2"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16,4a2,2 0,0 1,2 2v4.286l3.35,-2.871a1,1 0,0 1,1.65 0.759v7.652a1,1 0,0 1,-1.65 0.759L18,13.714V18a2,2 0,0 1,-2 2H6a4,4 0,0 1,-4 -4V8a4,4 0,0 1,4 -4zM9.55,9l-0.103,0.005a1,1 0,0 0,0 1.99L9.55,11h0.571l-2.828,2.828a1,1 0,0 0,1.414 1.414L11.55,12.4v0.6l0.005,0.102a1,1 0,0 0,1.99 0L13.55,13v-3l-0.005,-0.103A1,1 0,0 0,12.55 9z"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
</vector>

View File

@@ -4,12 +4,8 @@
android:autoMirrored="true" android:autoMirrored="true"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<group> <path
<clip-path android:pathData="M8.929,15.1a13.6,13.6 0,0 0,4.654 3.066q2.62,1.036 5.492,0.923h0.008l0.003,-0.004 0.003,-0.002 -0.034,-3.124 -3.52,-0.483 -1.791,1.792 -0.645,-0.322a13.5,13.5 0,0 1,-3.496 -2.52,13.4 13.4,0 0,1 -2.52,-3.496l-0.322,-0.645 1.792,-1.791 -0.483,-3.52 -3.123,-0.033 -0.003,0.002 -0.003,0.004v0.002a13.65,13.65 0,0 0,0.932 5.492A13.4,13.4 0,0 0,8.93 15.1m3.92,4.926a15.6,15.6 0,0 1,-5.334 -3.511,15.4 15.4,0 0,1 -3.505,-5.346 15.6,15.6 0,0 1,-1.069 -6.274,1.93 1.93,0 0,1 0.589,-1.366c0.366,-0.366 0.84,-0.589 1.386,-0.589h0.01l3.163,0.035a1.96,1.96 0,0 1,1.958 1.694v0.005l0.487,3.545v0.003c0.043,0.297 0.025,0.605 -0.076,0.907a2,2 0,0 1,-0.485 0.773l-0.762,0.762a11.3,11.3 0,0 0,1.806 2.348,11.4 11.4,0 0,0 2.348,1.806l0.762,-0.762a2,2 0,0 1,0.774 -0.485c0.302,-0.1 0.61,-0.118 0.907,-0.076l3.553,0.487a1.96,1.96 0,0 1,1.694 1.958l0.034,3.174c0,0.546 -0.223,1.02 -0.588,1.386 -0.36,0.36 -0.827,0.582 -1.363,0.588a15.3,15.3 0,0 1,-6.29 -1.062"
android:pathData="M0,0h24v24H0z"/> android:fillColor="#FF000000"
<path android:fillType="evenOdd"/>
android:pathData="M8.929,15.1a13.6,13.6 0,0 0,4.654 3.066q2.62,1.036 5.492,0.923h0.008l0.003,-0.004 0.003,-0.002 -0.034,-3.124 -3.52,-0.483 -1.791,1.792 -0.645,-0.322a13.5,13.5 0,0 1,-3.496 -2.52,13.4 13.4,0 0,1 -2.52,-3.496l-0.322,-0.644 1.792,-1.792 -0.483,-3.519 -3.123,-0.034 -0.003,0.002 -0.003,0.004v0.002a13.65,13.65 0,0 0,0.932 5.492A13.4,13.4 0,0 0,8.93 15.1m3.92,4.926a15.6,15.6 0,0 1,-5.334 -3.511,15.4 15.4,0 0,1 -3.505,-5.346 15.6,15.6 0,0 1,-1.069 -6.274,1.93 1.93,0 0,1 0.589,-1.366c0.366,-0.366 0.84,-0.589 1.386,-0.589h0.01l3.163,0.035a1.96,1.96 0,0 1,1.958 1.694v0.005l0.487,3.545v0.003c0.043,0.297 0.025,0.605 -0.076,0.907a2,2 0,0 1,-0.485 0.773l-0.762,0.762a11.4,11.4 0,0 0,3.206 3.54q0.457,0.33 0.948,0.614l0.762,-0.761a2,2 0,0 1,0.774 -0.486c0.302,-0.1 0.61,-0.118 0.907,-0.076l3.553,0.487a1.96,1.96 0,0 1,1.694 1.958l0.034,3.174c0,0.546 -0.223,1.02 -0.588,1.386 -0.361,0.36 -0.827,0.582 -1.363,0.588a15.3,15.3 0,0 1,-6.29 -1.062"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
</group>
</vector> </vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7.623,3.04a1.07,1.07 0,0 1,1.086 0.929l0.542,3.954q0.039,0.27 -0.038,0.504a1.1,1.1 0,0 1,-0.272 0.427l-1.64,1.64Q7.806,11.5 8.456,12.4c0.433,0.601 1.444,1.697 1.444,1.697 0.013,0.012 1.098,1.014 1.696,1.444q0.9,0.65 1.909,1.153l1.64,-1.64q0.194,-0.194 0.426,-0.27a1.1,1.1 0,0 1,0.504 -0.04l3.953,0.543q0.407,0.058 0.67,0.358 0.26,0.301 0.26,0.728l0.04,3.527q0,0.427 -0.33,0.756 -0.33,0.33 -0.756,0.33a16,16 0,0 1,-6.57 -1.105,16.2 16.2,0 0,1 -5.563,-3.663 16.1,16.1 0,0 1,-3.653 -5.573,16.3 16.3,0 0,1 -1.116,-6.56q0,-0.426 0.329,-0.756Q3.67,3 4.095,3zM20.25,3q0.405,0 0.707,0.3 0.3,0.301 0.3,0.708t-0.3,0.707l-1.414,1.414 1.414,1.414q0.3,0.3 0.3,0.707t-0.3,0.707 -0.707,0.3 -0.707,-0.3l-1.414,-1.414 -1.414,1.414q-0.3,0.3 -0.707,0.3t-0.707,-0.3T15,8.25q0,-0.406 0.3,-0.707l1.415,-1.414L15.3,4.715q-0.3,-0.3 -0.301,-0.707 0,-0.407 0.3,-0.707t0.71,-0.301q0.405,0 0.707,0.3l1.414,1.415L19.543,3.3q0.3,-0.3 0.707,-0.301"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7.623,3.04a1.07,1.07 0,0 1,1.086 0.929l0.542,3.954q0.039,0.27 -0.038,0.504a1.1,1.1 0,0 1,-0.272 0.427l-1.64,1.64Q7.806,11.5 8.456,12.4c0.433,0.601 1.444,1.697 1.444,1.697 0.013,0.012 1.098,1.014 1.696,1.444q0.9,0.65 1.909,1.153l1.64,-1.64q0.194,-0.194 0.426,-0.27a1.1,1.1 0,0 1,0.504 -0.04l3.953,0.543q0.407,0.058 0.67,0.358 0.26,0.301 0.26,0.728l0.04,3.527q0,0.427 -0.33,0.756 -0.33,0.33 -0.756,0.33a16,16 0,0 1,-6.57 -1.105,16.2 16.2,0 0,1 -5.563,-3.663 16.1,16.1 0,0 1,-3.653 -5.573,16.3 16.3,0 0,1 -1.116,-6.56q0,-0.426 0.329,-0.756Q3.67,3 4.095,3z"
android:fillColor="#FF000000"/>
<path
android:pathData="M16,5q0.425,0 0.713,0.287Q17,5.575 17,6a0.97,0.97 0,0 1,-0.287 0.713A0.97,0.97 0,0 1,16 7h-0.5l2.2,2.15 2.4,-2.4a0.95,0.95 0,0 1,0.7 -0.275,0.95 0.95,0 0,1 0.7,0.275q0.3,0.3 0.3,0.7a0.92,0.92 0,0 1,-0.275 0.675l-3.125,3.15a0.8,0.8 0,0 1,-0.312 0.225,1.04 1.04,0 0,1 -0.776,0 0.9,0.9 0,0 1,-0.312 -0.2l-3,-3V9a0.97,0.97 0,0 1,-0.287 0.713A0.97,0.97 0,0 1,13 10a0.97,0.97 0,0 1,-0.713 -0.287A0.97,0.97 0,0 1,12 9V6q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,13 5z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7.623,3.04a1.07,1.07 0,0 1,1.086 0.929l0.542,3.954q0.039,0.27 -0.038,0.504a1.1,1.1 0,0 1,-0.272 0.427l-1.64,1.64Q7.806,11.5 8.456,12.4c0.433,0.601 1.444,1.697 1.444,1.697 0.013,0.012 1.098,1.014 1.696,1.444q0.9,0.65 1.909,1.153l1.64,-1.64q0.194,-0.194 0.426,-0.27a1.1,1.1 0,0 1,0.504 -0.04l3.953,0.543q0.407,0.058 0.67,0.358 0.26,0.301 0.26,0.728l0.04,3.527q0,0.427 -0.33,0.756 -0.33,0.33 -0.756,0.33a16,16 0,0 1,-6.57 -1.105,16.2 16.2,0 0,1 -5.563,-3.663 16.1,16.1 0,0 1,-3.653 -5.573,16.3 16.3,0 0,1 -1.116,-6.56q0,-0.426 0.329,-0.756Q3.67,3 4.095,3z"
android:fillColor="#FF000000"/>
<path
android:pathData="M19.964,3a1,1 0,0 1,0.995 0.897l0.005,0.103v3l-0.005,0.103a1,1 0,0 1,-1.99 0L18.964,7v-0.605l-4.05,4.02A1,1 0,0 1,13.5 9l4.03,-4h-0.566l-0.103,-0.005a1,1 0,0 1,0 -1.99L16.964,3z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24H0z"/>
<path
android:pathData="M10.5,6.5q0.425,0 0.713,0.287 0.288,0.288 0.287,0.713v2h2q0.425,0 0.713,0.287 0.288,0.288 0.287,0.713a0.97,0.97 0,0 1,-0.287 0.713,0.97 0.97,0 0,1 -0.713,0.287h-2v2a0.97,0.97 0,0 1,-0.287 0.713,0.97 0.97,0 0,1 -0.713,0.287 0.97,0.97 0,0 1,-0.713 -0.287,0.97 0.97,0 0,1 -0.287,-0.713v-2h-2a0.97,0.97 0,0 1,-0.713 -0.287,0.97 0.97,0 0,1 -0.287,-0.713q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,7.5 9.5h2v-2q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,10.5 6.5"
android:fillColor="#FF000000"/>
<path
android:pathData="M10.5,3a7.5,7.5 0,0 1,5.963 12.049l3.244,3.244a1,1 0,1 1,-1.414 1.414l-3.244,-3.244A7.5,7.5 0,1 1,10.5 3m0,2a5.5,5.5 0,1 0,0 11,5.5 5.5,0 0,0 0,-11"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
<path
android:pathData="M15.05,16.463a7.5,7.5 0,1 1,1.414 -1.414l3.243,3.244a1,1 0,0 1,-1.414 1.414zM16,10.5a5.5,5.5 0,1 0,-11 0,5.5 5.5,0 0,0 11,0"
android:fillColor="#FF000000"/>
<path
android:pathData="M7.875,11.375h1.75v1.75q0,0.372 0.252,0.623A0.85,0.85 0,0 0,10.5 14a0.85,0.85 0,0 0,0.623 -0.252,0.85 0.85,0 0,0 0.252,-0.623v-1.75h1.75a0.85,0.85 0,0 0,0.623 -0.252A0.85,0.85 0,0 0,14 10.5a0.85,0.85 0,0 0,-0.252 -0.623,0.85 0.85,0 0,0 -0.623,-0.252h-1.75v-1.75a0.85,0.85 0,0 0,-0.252 -0.623A0.85,0.85 0,0 0,10.5 7a0.85,0.85 0,0 0,-0.623 0.252,0.85 0.85,0 0,0 -0.252,0.623v1.75h-1.75a0.85,0.85 0,0 0,-0.623 0.252A0.85,0.85 0,0 0,7 10.5q0,0.372 0.252,0.623a0.85,0.85 0,0 0,0.623 0.252"
android:fillColor="#FF000000"/>
</group>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.5,9.5q0.425,0 0.713,0.287 0.288,0.288 0.287,0.713a0.97,0.97 0,0 1,-0.287 0.713,0.97 0.97,0 0,1 -0.713,0.287h-6a0.97,0.97 0,0 1,-0.713 -0.287,0.97 0.97,0 0,1 -0.287,-0.713q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,7.5 9.5z"
android:fillColor="#FF000000"/>
<path
android:pathData="M10.5,3a7.5,7.5 0,0 1,5.963 12.049l3.244,3.244a1,1 0,1 1,-1.414 1.414l-3.244,-3.244A7.5,7.5 0,1 1,10.5 3m0,2a5.5,5.5 0,1 0,0 11,5.5 5.5,0 0,0 0,-11"
android:fillColor="#FF000000"
android:fillType="evenOdd"/>
</vector>

View File

@@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.colors.gradientCriticalColors
import io.element.android.libraries.designsystem.colors.gradientInfoColors
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.avatar.AvatarType
@@ -38,13 +40,16 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2392-6721
*/
@Composable @Composable
fun ComposerAlertMolecule( fun ComposerAlertMolecule(
avatar: AvatarData?, avatar: AvatarData?,
content: AnnotatedString, content: AnnotatedString,
onSubmitClick: () -> Unit, onSubmitClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
level: ComposerAlertLevel = ComposerAlertLevel.Default, level: ComposerAlertLevel = ComposerAlertLevel.Info,
showIcon: Boolean = false, showIcon: Boolean = false,
submitText: String = stringResource(CommonStrings.action_ok), submitText: String = stringResource(CommonStrings.action_ok),
) { ) {
@@ -52,20 +57,12 @@ fun ComposerAlertMolecule(
modifier.fillMaxWidth() modifier.fillMaxWidth()
) { ) {
val lineColor = when (level) { val lineColor = when (level) {
ComposerAlertLevel.Default -> ElementTheme.colors.borderInfoSubtle
ComposerAlertLevel.Info -> ElementTheme.colors.borderInfoSubtle ComposerAlertLevel.Info -> ElementTheme.colors.borderInfoSubtle
ComposerAlertLevel.Critical -> ElementTheme.colors.borderCriticalSubtle ComposerAlertLevel.Critical -> ElementTheme.colors.borderCriticalSubtle
} }
val startColor = when (level) {
ComposerAlertLevel.Default -> ElementTheme.colors.bgInfoSubtle
ComposerAlertLevel.Info -> ElementTheme.colors.bgInfoSubtle
ComposerAlertLevel.Critical -> ElementTheme.colors.bgCriticalSubtle
}
val textColor = when (level) { val textColor = when (level) {
ComposerAlertLevel.Default -> ElementTheme.colors.textPrimary ComposerAlertLevel.Info -> ElementTheme.colors.textPrimary
ComposerAlertLevel.Info -> ElementTheme.colors.textInfoPrimary
ComposerAlertLevel.Critical -> ElementTheme.colors.textCriticalPrimary ComposerAlertLevel.Critical -> ElementTheme.colors.textCriticalPrimary
} }
@@ -75,12 +72,13 @@ fun ComposerAlertMolecule(
.height(1.dp) .height(1.dp)
.background(lineColor) .background(lineColor)
) )
val brush = Brush.verticalGradient( val gradientColors = when (level) {
listOf(startColor, ElementTheme.colors.bgCanvasDefault), ComposerAlertLevel.Info -> gradientInfoColors()
) ComposerAlertLevel.Critical -> gradientCriticalColors()
}
Box( Box(
modifier = Modifier modifier = Modifier
.background(brush) .background(Brush.verticalGradient(gradientColors))
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
) { ) {
Column( Column(
@@ -96,12 +94,10 @@ fun ComposerAlertMolecule(
) )
} else if (showIcon) { } else if (showIcon) {
val icon = when (level) { val icon = when (level) {
ComposerAlertLevel.Default -> CompoundIcons.Info()
ComposerAlertLevel.Info -> CompoundIcons.Info() ComposerAlertLevel.Info -> CompoundIcons.Info()
ComposerAlertLevel.Critical -> CompoundIcons.Error() ComposerAlertLevel.Critical -> CompoundIcons.Error()
} }
val iconTint = when (level) { val iconTint = when (level) {
ComposerAlertLevel.Default -> ElementTheme.colors.iconPrimary
ComposerAlertLevel.Info -> ElementTheme.colors.iconInfoPrimary ComposerAlertLevel.Info -> ElementTheme.colors.iconInfoPrimary
ComposerAlertLevel.Critical -> ElementTheme.colors.iconCriticalPrimary ComposerAlertLevel.Critical -> ElementTheme.colors.iconCriticalPrimary
} }
@@ -131,7 +127,6 @@ fun ComposerAlertMolecule(
} }
enum class ComposerAlertLevel { enum class ComposerAlertLevel {
Default,
Info, Info,
Critical Critical
} }

View File

@@ -21,7 +21,6 @@ internal data class ComposerAlertMoleculeParams(
internal class ComposerAlertMoleculeParamsProvider : PreviewParameterProvider<ComposerAlertMoleculeParams> { internal class ComposerAlertMoleculeParamsProvider : PreviewParameterProvider<ComposerAlertMoleculeParams> {
private val allLevels = sequenceOf( private val allLevels = sequenceOf(
ComposerAlertLevel.Default,
ComposerAlertLevel.Info, ComposerAlertLevel.Info,
ComposerAlertLevel.Critical ComposerAlertLevel.Critical
) )

View File

@@ -38,8 +38,11 @@ fun gradientSubtleColors(): List<Color> = listOf(
fun gradientInfoColors(): List<Color> = listOf( fun gradientInfoColors(): List<Color> = listOf(
ElementTheme.colors.gradientInfoStop1, ElementTheme.colors.gradientInfoStop1,
ElementTheme.colors.gradientInfoStop2, ElementTheme.colors.gradientInfoStop2,
ElementTheme.colors.gradientInfoStop3, )
ElementTheme.colors.gradientInfoStop4,
ElementTheme.colors.gradientInfoStop5, @Composable
ElementTheme.colors.gradientInfoStop6, @ReadOnlyComposable
fun gradientCriticalColors(): List<Color> = listOf(
ElementTheme.colors.gradientCriticalStop1,
ElementTheme.colors.gradientCriticalStop2,
) )

View File

@@ -14,7 +14,6 @@ import io.element.android.libraries.designsystem.R
// All the icons should be defined in Compound. // All the icons should be defined in Compound.
internal val iconsOther = listOf( internal val iconsOther = listOf(
R.drawable.ic_notification, R.drawable.ic_notification,
R.drawable.ic_stop,
R.drawable.pin, R.drawable.pin,
R.drawable.ic_winner, R.drawable.ic_winner,
) )

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,16V8C6,7.45 6.196,6.979 6.588,6.588C6.979,6.196 7.45,6 8,6H16C16.55,6 17.021,6.196 17.413,6.588C17.804,6.979 18,7.45 18,8V16C18,16.55 17.804,17.021 17.413,17.413C17.021,17.804 16.55,18 16,18H8C7.45,18 6.979,17.804 6.588,17.413C6.196,17.021 6,16.55 6,16Z"
android:fillColor="#ffffff"/>
</vector>

View File

@@ -22,6 +22,11 @@ sealed class NotificationResolverException : Exception() {
*/ */
data object EventFilteredOut : NotificationResolverException() data object EventFilteredOut : NotificationResolverException()
/**
* The event was found but it has been redacted.
*/
data object EventRedacted : NotificationResolverException()
/** /**
* An unexpected error occurred while trying to resolve the event. * An unexpected error occurred while trying to resolve the event.
*/ */

View File

@@ -33,13 +33,33 @@ sealed class ErrorType(message: String) : Exception(message) {
*/ */
class NotFound(message: String) : ErrorType(message) class NotFound(message: String) : ErrorType(message)
/**
* The device could not be created.
*/
class UnableToCreateDevice(message: String) : ErrorType(message)
/** /**
* An unknown error has happened. * An unknown error has happened.
*/ */
class Unknown(message: String) : ErrorType(message) class Unknown(message: String) : ErrorType(message)
/**
* The requested device was not returned by the homeserver.
*/
class DeviceNotFound(message: String) : ErrorType(message)
/**
* The other device is already signed in and so does not need to sign in.
*/
class OtherDeviceAlreadySignedIn(message: String) : ErrorType(message)
/**
* The sign in was cancelled.
*/
class Cancelled(message: String) : ErrorType(message)
/**
* The sign in was not completed in the required time.
*/
class Expired(message: String) : ErrorType(message)
/**
* A secure connection could not have been established between the two devices.
*/
class ConnectionInsecure(message: String) : ErrorType(message)
} }

View File

@@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.api.media package io.element.android.libraries.matrix.api.media
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -16,9 +17,20 @@ data class MediaSource(
/** /**
* Url of the media. * Url of the media.
*/ */
val url: String, private val url: String,
/** /**
* This is used to hold data for encrypted media. * This is used to hold data for encrypted media.
*/ */
val json: String? = null, val json: String? = null,
) : Parcelable ) : Parcelable {
/**
* A URL with invalid parts (like `#fragment`, if it's an MXC url) removed.
*/
@IgnoredOnParcel
val safeUrl = if (url.startsWith("mxc")) {
// We've seen some MXC urls in the wild having some `mxc://foo/bar#auto` fragment suffix, which is invalid
url.substringBefore("#")
} else {
url
}
}

View File

@@ -35,6 +35,11 @@ interface RoomListService {
data object Hide : SyncIndicator data object Hide : SyncIndicator
} }
/**
* Indicates whether the initial sliding sync request is done or not.
*/
val isInitialSyncDone: Boolean
/** /**
* Creates a room list that can be used to load more rooms and filter them dynamically. * Creates a room list that can be used to load more rooms and filter them dynamically.
* @param pageSize the number of rooms to load at once. * @param pageSize the number of rooms to load at once.

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.media
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.media.aMediaSource
import org.junit.Test
class MediaSourceTest {
@Test
fun `safeUrl removes the fragment part in MXC urls`() {
val mediaSource = aMediaSource(url = "mxc://matrix.org/url#fragment")
assertThat(mediaSource.safeUrl).isEqualTo("mxc://matrix.org/url")
}
@Test
fun `safeUrl keeps the fragment part in a non-MXC url`() {
val mediaSource = aMediaSource(url = "https://matrix.org/url#fragment")
assertThat(mediaSource.safeUrl).isEqualTo("https://matrix.org/url#fragment")
}
}

View File

@@ -83,7 +83,7 @@ import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
import io.element.android.libraries.matrix.impl.workmanager.PerformDatabaseVacuumWorkManagerRequest import io.element.android.libraries.matrix.impl.workmanager.PerformDatabaseVacuumRequestBuilder
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.workmanager.api.WorkManagerRequestType import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerScheduler import io.element.android.libraries.workmanager.api.WorkManagerScheduler
@@ -832,8 +832,8 @@ class RustMatrixClient(
if (workManagerScheduler.hasPendingWork(sessionId, WorkManagerRequestType.DB_VACUUM)) return if (workManagerScheduler.hasPendingWork(sessionId, WorkManagerRequestType.DB_VACUUM)) return
Timber.i("Scheduling periodic database vacuuming for session $sessionId") Timber.i("Scheduling periodic database vacuuming for session $sessionId")
val request = PerformDatabaseVacuumWorkManagerRequest(sessionId) val request = PerformDatabaseVacuumRequestBuilder(sessionId)
workManagerScheduler.submit(request) sessionCoroutineScope.launch { workManagerScheduler.submit(request) }
} }
} }

View File

@@ -15,7 +15,11 @@ internal fun HumanQrGrantLoginException.map() = when (this) {
is HumanQrGrantLoginException.InvalidCheckCode -> ErrorType.InvalidCheckCode(message.orEmpty()) is HumanQrGrantLoginException.InvalidCheckCode -> ErrorType.InvalidCheckCode(message.orEmpty())
is HumanQrGrantLoginException.MissingSecretsBackup -> ErrorType.MissingSecretsBackup(message.orEmpty()) is HumanQrGrantLoginException.MissingSecretsBackup -> ErrorType.MissingSecretsBackup(message.orEmpty())
is HumanQrGrantLoginException.NotFound -> ErrorType.NotFound(message.orEmpty()) is HumanQrGrantLoginException.NotFound -> ErrorType.NotFound(message.orEmpty())
is HumanQrGrantLoginException.UnableToCreateDevice -> ErrorType.UnableToCreateDevice(message.orEmpty()) is HumanQrGrantLoginException.Cancelled -> ErrorType.Cancelled(message.orEmpty())
is HumanQrGrantLoginException.ConnectionInsecure -> ErrorType.ConnectionInsecure(message.orEmpty())
is HumanQrGrantLoginException.DeviceNotFound -> ErrorType.DeviceNotFound(message.orEmpty())
is HumanQrGrantLoginException.Expired -> ErrorType.Expired(message.orEmpty())
is HumanQrGrantLoginException.OtherDeviceAlreadySignedIn -> ErrorType.OtherDeviceAlreadySignedIn(message.orEmpty())
is HumanQrGrantLoginException.Unknown -> ErrorType.Unknown(message.orEmpty()) is HumanQrGrantLoginException.Unknown -> ErrorType.Unknown(message.orEmpty())
is HumanQrGrantLoginException.UnsupportedProtocol -> ErrorType.UnsupportedProtocol(message.orEmpty()) is HumanQrGrantLoginException.UnsupportedProtocol -> ErrorType.UnsupportedProtocol(message.orEmpty())
} }

View File

@@ -91,7 +91,7 @@ class RustMediaLoader(
return if (json != null) { return if (json != null) {
RustMediaSource.fromJson(json) RustMediaSource.fromJson(json)
} else { } else {
RustMediaSource.fromUrl(url) RustMediaSource.fromUrl(safeUrl)
} }
} }
} }

View File

@@ -66,6 +66,10 @@ class RustNotificationService(
Timber.d("Could not retrieve event for notification with $eventId - event filtered out") Timber.d("Could not retrieve event for notification with $eventId - event filtered out")
put(eventId, Result.failure(NotificationResolverException.EventFilteredOut)) put(eventId, Result.failure(NotificationResolverException.EventFilteredOut))
} }
NotificationStatus.EventRedacted -> {
Timber.d("Could not retrieve event for notification with $eventId - event redacted")
put(eventId, Result.failure(NotificationResolverException.EventRedacted))
}
} }
} }
is BatchNotificationResult.Error -> { is BatchNotificationResult.Error -> {

View File

@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
internal class RustRoomListService( internal class RustRoomListService(
@@ -33,6 +34,9 @@ internal class RustRoomListService(
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
) : RoomListService { ) : RoomListService {
private val _isInitialSyncDone = AtomicBoolean(false)
override val isInitialSyncDone: Boolean get() = _isInitialSyncDone.get()
override fun createRoomList( override fun createRoomList(
pageSize: Int, pageSize: Int,
source: RoomList.Source, source: RoomList.Source,
@@ -75,6 +79,9 @@ internal class RustRoomListService(
.map { it.toRoomListState() } .map { it.toRoomListState() }
.onEach { state -> .onEach { state ->
Timber.d("RoomList state=$state") Timber.d("RoomList state=$state")
if (state == RoomListService.State.Running) {
_isInitialSyncDone.set(true)
}
} }
.distinctUntilChanged() .distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle) .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle)

View File

@@ -21,11 +21,11 @@ import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineInterface import org.matrix.rustcomponents.sdk.TimelineInterface
import org.matrix.rustcomponents.sdk.TimelineListener import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk.RoomPaginationStatus import uniffi.matrix_sdk.PaginationStatus
internal fun TimelineInterface.liveBackPaginationStatus(): Flow<RoomPaginationStatus> = callbackFlow { internal fun TimelineInterface.liveBackPaginationStatus(): Flow<PaginationStatus> = callbackFlow {
val listener = object : PaginationStatusListener { val listener = object : PaginationStatusListener {
override fun onUpdate(status: RoomPaginationStatus) { override fun onUpdate(status: PaginationStatus) {
trySend(status) trySend(status)
} }
} }

View File

@@ -71,7 +71,7 @@ import org.matrix.rustcomponents.sdk.UploadParameters
import org.matrix.rustcomponents.sdk.UploadSource import org.matrix.rustcomponents.sdk.UploadSource
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk.RoomPaginationStatus import uniffi.matrix_sdk.PaginationStatus
import java.io.File import java.io.File
import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@@ -147,8 +147,8 @@ class RustTimeline(
.onEach { backPaginationStatus -> .onEach { backPaginationStatus ->
updatePaginationStatus(Timeline.PaginationDirection.BACKWARDS) { updatePaginationStatus(Timeline.PaginationDirection.BACKWARDS) {
when (backPaginationStatus) { when (backPaginationStatus) {
is RoomPaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitTimelineStart) is PaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitTimelineStart)
is RoomPaginationStatus.Paginating -> it.copy(isPaginating = true, hasMoreToLoad = true) is PaginationStatus.Paginating -> it.copy(isPaginating = true, hasMoreToLoad = true)
} }
} }
} }

View File

@@ -10,18 +10,18 @@ package io.element.android.libraries.matrix.impl.workmanager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.Data import androidx.work.Data
import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequest
import androidx.work.WorkRequest
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.workmanager.VacuumDatabaseWorker.Companion.SESSION_ID_PARAM import io.element.android.libraries.matrix.impl.workmanager.VacuumDatabaseWorker.Companion.SESSION_ID_PARAM
import io.element.android.libraries.workmanager.api.WorkManagerRequest import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
import io.element.android.libraries.workmanager.api.WorkManagerRequestType import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper
import io.element.android.libraries.workmanager.api.workManagerTag import io.element.android.libraries.workmanager.api.workManagerTag
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class PerformDatabaseVacuumWorkManagerRequest( class PerformDatabaseVacuumRequestBuilder(
private val sessionId: SessionId, private val sessionId: SessionId,
) : WorkManagerRequest { ) : WorkManagerRequestBuilder {
override fun build(): Result<List<WorkRequest>> { override suspend fun build(): Result<List<WorkManagerRequestWrapper>> {
val data = Data.Builder().putString(SESSION_ID_PARAM, sessionId.value).build() val data = Data.Builder().putString(SESSION_ID_PARAM, sessionId.value).build()
val workRequest = PeriodicWorkRequest.Builder( val workRequest = PeriodicWorkRequest.Builder(
workerClass = VacuumDatabaseWorker::class, workerClass = VacuumDatabaseWorker::class,
@@ -41,6 +41,6 @@ class PerformDatabaseVacuumWorkManagerRequest(
) )
.build() .build()
return Result.success(listOf(workRequest)) return Result.success(listOf(WorkManagerRequestWrapper(workRequest)))
} }
} }

View File

@@ -19,7 +19,7 @@ import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.libraries.workmanager.api.WorkManagerRequest import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@@ -33,7 +33,7 @@ import java.io.File
class RustMatrixClientFactoryTest { class RustMatrixClientFactoryTest {
@Test @Test
fun test() = runTest { fun test() = runTest {
val scheduleVacuumLambda = lambdaRecorder<WorkManagerRequest, Unit> {} val scheduleVacuumLambda = lambdaRecorder<WorkManagerRequestBuilder, Unit> {}
val workManagerScheduler = FakeWorkManagerScheduler(submitLambda = scheduleVacuumLambda) val workManagerScheduler = FakeWorkManagerScheduler(submitLambda = scheduleVacuumLambda)
val sut = createRustMatrixClientFactory(workManagerScheduler = workManagerScheduler) val sut = createRustMatrixClientFactory(workManagerScheduler = workManagerScheduler)

View File

@@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.Timeline
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener import org.matrix.rustcomponents.sdk.TimelineListener
import uniffi.matrix_sdk.RoomPaginationStatus import uniffi.matrix_sdk.PaginationStatus
class FakeFfiTimeline : Timeline(NoHandle) { class FakeFfiTimeline : Timeline(NoHandle) {
private var listener: TimelineListener? = null private var listener: TimelineListener? = null
@@ -33,7 +33,7 @@ class FakeFfiTimeline : Timeline(NoHandle) {
return FakeFfiTaskHandle() return FakeFfiTaskHandle()
} }
fun emitPaginationStatus(status: RoomPaginationStatus) { fun emitPaginationStatus(status: PaginationStatus) {
paginationStatusListener!!.onUpdate(status) paginationStatusListener!!.onUpdate(status)
} }

View File

@@ -32,7 +32,7 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import uniffi.matrix_sdk.RoomPaginationStatus import uniffi.matrix_sdk.PaginationStatus
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
class RustTimelineTest { class RustTimelineTest {
@@ -68,10 +68,10 @@ class RustTimelineTest {
// Start pagination // Start pagination
sut.paginate(Timeline.PaginationDirection.BACKWARDS) sut.paginate(Timeline.PaginationDirection.BACKWARDS)
// Simulate SDK starting pagination // Simulate SDK starting pagination
inner.emitPaginationStatus(RoomPaginationStatus.Paginating) inner.emitPaginationStatus(PaginationStatus.Paginating)
// No new events received // No new events received
// Simulate SDK stopping pagination, more event to load // Simulate SDK stopping pagination, more event to load
inner.emitPaginationStatus(RoomPaginationStatus.Idle(hitTimelineStart = false)) inner.emitPaginationStatus(PaginationStatus.Idle(hitTimelineStart = false))
// expect an item to be emitted, with an updated timestamp // expect an item to be emitted, with an updated timestamp
with(awaitItem()) { with(awaitItem()) {
assertThat(size).isEqualTo(2) assertThat(size).isEqualTo(2)

View File

@@ -20,10 +20,14 @@ class FakeRoomListService(
private val subscribeToVisibleRoomsLambda: (List<RoomId>) -> Unit = {}, private val subscribeToVisibleRoomsLambda: (List<RoomId>) -> Unit = {},
private val createRoomListLambda: (pageSize: Int) -> DynamicRoomList = { pageSize -> FakeDynamicRoomList(pageSize = pageSize) }, private val createRoomListLambda: (pageSize: Int) -> DynamicRoomList = { pageSize -> FakeDynamicRoomList(pageSize = pageSize) },
override val allRooms: RoomList = createRoomListLambda(Int.MAX_VALUE), override val allRooms: RoomList = createRoomListLambda(Int.MAX_VALUE),
private val isInitialSyncLambda: () -> Boolean = { true },
) : RoomListService { ) : RoomListService {
private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle) private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle)
private val syncIndicatorStateFlow = MutableStateFlow<RoomListService.SyncIndicator>(RoomListService.SyncIndicator.Hide) private val syncIndicatorStateFlow = MutableStateFlow<RoomListService.SyncIndicator>(RoomListService.SyncIndicator.Hide)
override val isInitialSyncDone: Boolean
get() = isInitialSyncLambda()
suspend fun postState(state: RoomListService.State) { suspend fun postState(state: RoomListService.State) {
roomListStateFlow.emit(state) roomListStateFlow.emit(state)
} }

View File

@@ -27,15 +27,15 @@ internal class CoilMediaFetcher(
private val mediaData: MediaRequestData, private val mediaData: MediaRequestData,
) : Fetcher { ) : Fetcher {
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
val source = mediaData.source val mediaSource = mediaData.source
if (source == null) { if (mediaSource == null) {
Timber.e("MediaData source is null") Timber.e("MediaData source is null")
return null return null
} }
return when (val kind = mediaData.kind) { return when (val kind = mediaData.kind) {
is MediaRequestData.Kind.Content -> fetchContent(source) is MediaRequestData.Kind.Content -> fetchContent(mediaSource)
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(source, kind) is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaSource, kind)
is MediaRequestData.Kind.File -> fetchFile(source, kind) is MediaRequestData.Kind.File -> fetchFile(mediaSource, kind)
} }
} }

View File

@@ -25,5 +25,5 @@ internal class MediaRequestDataKeyer : Keyer<MediaRequestData> {
} }
private fun MediaRequestData.toKey(): String? { private fun MediaRequestData.toKey(): String? {
return source?.let { "${it.url}_$kind" } return source?.let { "${it.safeUrl}_$kind" }
} }

View File

@@ -126,7 +126,7 @@ class MediaViewerDataSource(
when (mediaItem) { when (mediaItem) {
is MediaItem.DateSeparator -> Unit is MediaItem.DateSeparator -> Unit
is MediaItem.Event -> { is MediaItem.Event -> {
val sourceUrl = mediaItem.mediaSource().url val sourceUrl = mediaItem.mediaSource().safeUrl
val localMedia = localMediaStates.getOrPut(sourceUrl) { val localMedia = localMediaStates.getOrPut(sourceUrl) {
mutableStateOf(AsyncData.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
@@ -153,7 +153,7 @@ class MediaViewerDataSource(
}.toImmutableList() }.toImmutableList()
fun clearLoadingError(data: MediaViewerPageData.MediaViewerData) { fun clearLoadingError(data: MediaViewerPageData.MediaViewerData) {
localMediaStates[data.mediaSource.url]?.value = AsyncData.Uninitialized localMediaStates[data.mediaSource.safeUrl]?.value = AsyncData.Uninitialized
} }
suspend fun loadMore(direction: Timeline.PaginationDirection) { suspend fun loadMore(direction: Timeline.PaginationDirection) {
@@ -162,7 +162,7 @@ class MediaViewerDataSource(
suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) { suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) {
Timber.d("loadMedia for ${data.eventId}") Timber.d("loadMedia for ${data.eventId}")
val localMediaState = localMediaStates.getOrPut(data.mediaSource.url) { val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) {
mutableStateOf(AsyncData.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
localMediaState.value = AsyncData.Loading() localMediaState.value = AsyncData.Loading()

View File

@@ -10,8 +10,8 @@ package io.element.android.libraries.permissions.impl.troubleshoot
import android.Manifest import android.Manifest
import android.os.Build import android.os.Build
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet import dev.zacsweers.metro.ContributesIntoSet
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.R import io.element.android.libraries.permissions.impl.R
import io.element.android.libraries.permissions.impl.action.PermissionActions import io.element.android.libraries.permissions.impl.action.PermissionActions
@@ -24,7 +24,7 @@ import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ContributesIntoSet(AppScope::class) @ContributesIntoSet(SessionScope::class)
class NotificationTroubleshootCheckPermissionTest( class NotificationTroubleshootCheckPermissionTest(
private val permissionStateProvider: PermissionStateProvider, private val permissionStateProvider: PermissionStateProvider,
private val sdkVersionProvider: BuildVersionSdkIntProvider, private val sdkVersionProvider: BuildVersionSdkIntProvider,

View File

@@ -1,20 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.api.push
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
data class NotificationEventRequest(
val sessionId: SessionId,
val roomId: RoomId,
val eventId: EventId,
val providerInfo: String,
)

View File

@@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.api.push
fun interface SyncOnNotifiableEvent {
suspend operator fun invoke(requests: List<NotificationEventRequest>)
}

View File

@@ -97,6 +97,7 @@ sqldelight {
databases { databases {
create("PushDatabase") { create("PushDatabase") {
schemaOutputDirectory = File("src/main/sqldelight/databases") schemaOutputDirectory = File("src/main/sqldelight/databases")
verifyMigrations = true
} }
} }
} }

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