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)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.preferences.test)

View File

@@ -8,7 +8,6 @@
package io.element.android.appnav
import android.content.Intent
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
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.securebackup.api.SecureBackupEntryPoint
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.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@@ -307,7 +307,7 @@ class LoggedInFlowNode(
data object RoomDirectory : NavTarget
@Parcelize
data class IncomingShare(val intent: Intent) : NavTarget
data class IncomingShare(val shareIntentData: ShareIntentData) : NavTarget
@Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
@@ -570,7 +570,7 @@ class LoggedInFlowNode(
shareEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(intent = navTarget.intent),
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) {
// 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 ->
navTarget is NavTarget.Home
}
attachChild<Node> {
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.accesscontrol.AccountProviderAccessControl
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.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.BackstackView
@@ -265,7 +266,7 @@ class RootFlowNode(
@Parcelize data class AccountSelect(
val currentSessionId: SessionId,
val intent: Intent?,
val shareIntentData: ShareIntentData?,
val permalinkData: PermalinkData?,
) : NavTarget
@@ -357,8 +358,8 @@ class RootFlowNode(
backstack.pop()
}
attachSession(sessionId).apply {
if (navTarget.intent != null) {
attachIncomingShare(navTarget.intent)
if (navTarget.shareIntentData != null) {
attachIncomingShare(navTarget.shareIntentData)
} else if (navTarget.permalinkData != null) {
attachPermalinkData(navTarget.permalinkData)
}
@@ -392,7 +393,7 @@ class RootFlowNode(
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
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?
val latestSessionId = sessionStore.getLatestSessionId()
if (latestSessionId == null) {
@@ -437,13 +438,13 @@ class RootFlowNode(
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
intent = intent,
shareIntentData = shareIntentData,
permalinkData = null,
)
)
} else {
// Only one account, directly attach the incoming share node.
loggedInFlowNode.attachIncomingShare(intent)
loggedInFlowNode.attachIncomingShare(shareIntentData)
}
}
}
@@ -467,7 +468,7 @@ class RootFlowNode(
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
intent = null,
shareIntentData = null,
permalinkData = permalinkData,
)
)

View File

@@ -12,6 +12,8 @@ import android.content.Intent
import dev.zacsweers.metro.Inject
import io.element.android.features.login.api.LoginIntentResolver
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.DeeplinkParser
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 Permalink(val permalinkData: PermalinkData) : ResolvedIntent
data class Login(val params: LoginParams) : ResolvedIntent
data class IncomingShare(val intent: Intent) : ResolvedIntent
data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent
}
@Inject
@@ -34,6 +36,7 @@ class IntentResolver(
private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser,
private val shareIntentHandler: ShareIntentHandler,
) {
fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null
@@ -62,7 +65,8 @@ class IntentResolver(
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
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

View File

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

View File

@@ -144,8 +144,8 @@ Prerequisites:
```
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
the questions.
[`tools/sdk/build-rust-sdk`](../tools/sdk/build-rust-sdk). Type
`./tools/sdk/build-rust-sdk --help` for help.
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

View File

@@ -189,9 +189,13 @@ class LinkNewDeviceFlowNode(
is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected
is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError
is ErrorType.NotFound -> ErrorScreenType.Expired
is ErrorType.UnableToCreateDevice -> ErrorScreenType.UnknownError
is ErrorType.DeviceNotFound -> ErrorScreenType.UnknownError
is ErrorType.Unknown -> 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,
// 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.FakeMatrixClient
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.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -149,7 +150,7 @@ class LogoutPresenterTest {
@Test
fun `present - logout then confirm`() = runTest {
val cancelWorkManagerJobsLambda = lambdaRecorder<SessionId, Unit> {}
val cancelWorkManagerJobsLambda = lambdaRecorder<SessionId, WorkManagerRequestType?, Unit> { _, _ -> }
val workManagerScheduler = FakeWorkManagerScheduler(cancelLambda = cancelWorkManagerJobsLambda)
val presenter = createLogoutPresenter(workManagerScheduler = workManagerScheduler)
moleculeFlow(RecompositionMode.Immediate) {
@@ -238,7 +239,7 @@ class LogoutPresenterTest {
internal fun createLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = {}),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(cancelLambda = { _, _ -> }),
): LogoutPresenter = LogoutPresenter(
matrixClient = matrixClient,
encryptionService = encryptionService,

View File

@@ -115,7 +115,7 @@ private fun ViolationAlert(
},
submitText = stringResource(submitTextId),
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
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.sessionStorage.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.workmanager.api)
testCommonDependencies(libs)
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
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
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
interface ShareEntryPoint : FeatureEntryPoint {
data class Params(val intent: Intent)
data class Params(val shareIntentData: ShareIntentData)
fun createNode(
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)
testCommonDependencies(libs, true)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaupload.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>(
buildContext = buildContext,
plugins = listOf(
ShareNode.Inputs(intent = params.intent),
ShareNode.Inputs(shareIntentData = params.shareIntentData),
callback,
)
)

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
* 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.
@@ -14,10 +13,12 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import androidx.core.content.IntentCompat
import dev.zacsweers.metro.AppScope
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.core.mimetype.MimeTypes
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 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)
class DefaultShareIntentHandler(
@ApplicationContext private val context: Context,
) : ShareIntentHandler {
override suspend fun handleIncomingShareIntent(
override fun handleIncomingShareIntent(
intent: Intent,
onUris: suspend (List<ShareIntentHandler.UriToShare>) -> Boolean,
onPlainText: suspend (String) -> Boolean,
): Boolean {
val type = intent.resolveType(context) ?: return false
): ShareIntentData? {
val type = intent.resolveType(context) ?: return null
val uris = getIncomingUris(intent, type)
return when {
uris.isEmpty() && type == MimeTypes.PlainText -> handlePlainText(intent, onPlainText)
uris.isEmpty() && type == MimeTypes.PlainText -> handlePlainText(intent)
type.isMimeTypeImage() ||
type.isMimeTypeVideo() ||
type.isMimeTypeAudio() ||
@@ -68,20 +49,21 @@ class DefaultShareIntentHandler(
type.isMimeTypeFile() ||
type.isMimeTypeText() ||
type.isMimeTypeAny() -> {
val result = onUris(uris)
revokeUriPermissions(uris.map { it.uri })
result
ShareIntentData.Uris(
text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()?.takeIf { it.isNotEmpty() },
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()
return if (content?.isNotEmpty() == true) {
onPlainText(content)
ShareIntentData.PlainText(content)
} else {
false
null
}
}
@@ -89,7 +71,7 @@ class DefaultShareIntentHandler(
* 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.
*/
private fun getIncomingUris(intent: Intent, fallbackMimeType: String): List<ShareIntentHandler.UriToShare> {
private fun getIncomingUris(intent: Intent, fallbackMimeType: String): List<UriToShare> {
val uriList = mutableListOf<Uri>()
if (intent.action == Intent.ACTION_SEND) {
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
// in the same intent (i.e. 'image/*'). We need to check the mime type of each uri.
val mimeType = context.contentResolver.getType(uri) ?: fallbackMimeType
ShareIntentHandler.UriToShare(
UriToShare(
uri = uri,
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
import android.content.Intent
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
@@ -23,6 +22,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
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.callback
import io.element.android.libraries.architecture.inputs
@@ -50,10 +50,10 @@ class ShareNode(
@Parcelize
object NavTarget : Parcelable
data class Inputs(val intent: Intent) : NodeInputs
data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs
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()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {

View File

@@ -8,13 +8,14 @@
package io.element.android.features.share.impl
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
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.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -32,24 +33,24 @@ import kotlin.coroutines.cancellation.CancellationException
@AssistedInject
class SharePresenter(
@Assisted private val intent: Intent,
@Assisted private val shareIntentData: ShareIntentData,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val shareIntentHandler: ShareIntentHandler,
private val matrixClient: MatrixClient,
private val mediaSenderRoomFactory: MediaSenderRoomFactory,
private val activeRoomsHolder: ActiveRoomsHolder,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
private val onSharedData: OnSharedData,
) : Presenter<ShareState> {
@AssistedFactory
fun interface Factory {
fun create(intent: Intent): SharePresenter
fun create(shareIntentData: ShareIntentData): SharePresenter
}
private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List<RoomId>) {
sessionCoroutineScope.share(intent, roomIds)
sessionCoroutineScope.share(shareIntentData, roomIds)
}
@Composable
@@ -73,13 +74,24 @@ class SharePresenter(
}
private fun CoroutineScope.share(
intent: Intent,
shareIntentData: ShareIntentData,
roomIds: List<RoomId>,
) = launch {
suspend {
val result = shareIntentHandler.handleIncomingShareIntent(
intent,
onUris = { filesToShare ->
val result = when (shareIntentData) {
is ShareIntentData.PlainText -> {
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()) {
false
} else {
@@ -90,6 +102,7 @@ class SharePresenter(
filesToShare
.map { fileToShare ->
val result = mediaSender.sendMedia(
caption = shareIntentData.text,
uri = fileToShare.uri,
mimeType = fileToShare.mimeType,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
@@ -113,19 +126,12 @@ class SharePresenter(
}
.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) {
error("Failed to handle incoming share intent")
}

View File

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

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
import android.content.Intent
import android.net.Uri
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient
@@ -72,8 +74,17 @@ class SharePresenterTest {
@Test
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(
shareIntentHandler = FakeShareIntentHandler { _, _, _ -> true }
matrixClient = matrixClient,
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -100,9 +111,7 @@ class SharePresenterTest {
}
val presenter = createSharePresenter(
matrixClient = matrixClient,
shareIntentHandler = FakeShareIntentHandler { _, _, onText ->
onText(A_MESSAGE)
}
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -131,16 +140,15 @@ class SharePresenterTest {
)
val presenter = createSharePresenter(
matrixClient = matrixClient,
shareIntentHandler = FakeShareIntentHandler { _, onFile, _ ->
onFile(
listOf(
ShareIntentHandler.UriToShare(
uri = Uri.parse("content://image.jpg"),
mimeType = MimeTypes.Jpeg,
)
shareIntentData = ShareIntentData.Uris(
text = A_MESSAGE,
listOf(
UriToShare(
uri = Uri.parse("content://image.jpg"),
mimeType = MimeTypes.Jpeg,
)
)
},
),
mediaSenderRoomFactory = MediaSenderRoomFactory { mediaSender },
)
moleculeFlow(RecompositionMode.Immediate) {
@@ -159,20 +167,20 @@ class SharePresenterTest {
}
internal fun TestScope.createSharePresenter(
intent: Intent = Intent(),
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
shareIntentData: ShareIntentData = ShareIntentData.PlainText(A_MESSAGE),
matrixClient: MatrixClient = FakeMatrixClient(),
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { FakeMediaSender() },
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
onSharedData: OnSharedData = OnSharedData {},
): SharePresenter {
return SharePresenter(
intent = intent,
shareIntentData = shareIntentData,
sessionCoroutineScope = this,
shareIntentHandler = shareIntentHandler,
matrixClient = matrixClient,
activeRoomsHolder = activeRoomsHolder,
mediaSenderRoomFactory = mediaSenderRoomFactory,
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"
# Dependency analysis
dependencyAnalysis = "3.6.0"
dependencyAnalysis = "3.6.1"
# DI
metro = "0.11.1"
metro = "0.11.2"
# Auto service
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
# 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.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.0"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.1"
# Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@@ -220,7 +220,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics
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
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,
"gradientActionStop3" to gradientActionStop3,
"gradientActionStop4" to gradientActionStop4,
"gradientCriticalStop1" to gradientCriticalStop1,
"gradientCriticalStop2" to gradientCriticalStop2,
"gradientInfoStop1" to gradientInfoStop1,
"gradientInfoStop2" to gradientInfoStop2,
"gradientInfoStop3" to gradientInfoStop3,
"gradientInfoStop4" to gradientInfoStop4,
"gradientInfoStop5" to gradientInfoStop5,
"gradientInfoStop6" to gradientInfoStop6,
"gradientSubtleStop1" to gradientSubtleStop1,
"gradientSubtleStop2" to gradientSubtleStop2,
"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.
* Please see LICENSE files in the repository root for full details.
@@ -25,6 +25,9 @@ object CompoundIcons {
@Composable fun Admin(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_arrow_down)
}
@@ -64,6 +67,9 @@ object CompoundIcons {
@Composable fun Bold(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_bold)
}
@Composable fun Bug(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_bug)
}
@Composable fun Calendar(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_calendar)
}
@@ -460,6 +466,9 @@ object CompoundIcons {
@Composable fun RaisedHandSolid(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_reaction)
}
@@ -478,9 +487,18 @@ object CompoundIcons {
@Composable fun Room(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_search)
}
@Composable fun Section(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_section)
}
@Composable fun Send(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_send)
}
@@ -535,6 +553,12 @@ object CompoundIcons {
@Composable fun Sticker(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_strikethrough)
}
@@ -550,6 +574,9 @@ object CompoundIcons {
@Composable fun TextFormatting(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_threads)
}
@@ -559,6 +586,12 @@ object CompoundIcons {
@Composable fun Time(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_underline)
}
@@ -607,6 +640,9 @@ object CompoundIcons {
@Composable fun VideoCallOffSolid(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_solid)
}
@@ -619,6 +655,15 @@ object CompoundIcons {
@Composable fun VoiceCall(): ImageVector {
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 {
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_solid)
}
@@ -643,9 +688,16 @@ object CompoundIcons {
@Composable fun Windows(): ImageVector {
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>(
Admin(),
AdvancedSettings(),
ArrowDown(),
ArrowLeft(),
ArrowRight(),
@@ -659,6 +711,7 @@ object CompoundIcons {
BackspaceSolid(),
Block(),
Bold(),
Bug(),
Calendar(),
Chart(),
Chat(),
@@ -791,13 +844,17 @@ object CompoundIcons {
QrCode(),
Quote(),
RaisedHandSolid(),
ReOrder(),
Reaction(),
ReactionAdd(),
ReactionSolid(),
Reply(),
Restart(),
Room(),
RotateLeft(),
RotateRight(),
Search(),
Section(),
Send(),
SendSolid(),
Settings(),
@@ -816,14 +873,19 @@ object CompoundIcons {
Spotlight(),
SpotlightView(),
Sticker(),
Stop(),
StopSolid(),
Strikethrough(),
SwitchCameraSolid(),
TakePhoto(),
TakePhotoSolid(),
TextFormatting(),
Theme(),
Threads(),
ThreadsSolid(),
Time(),
Translate(),
Tree(),
Underline(),
Unknown(),
UnknownSolid(),
@@ -840,10 +902,14 @@ object CompoundIcons {
VideoCallMissedSolid(),
VideoCallOff(),
VideoCallOffSolid(),
VideoCallOutgoingSolid(),
VideoCallSolid(),
VisibilityOff(),
VisibilityOn(),
VoiceCall(),
VoiceCallDeclinedSolid(),
VoiceCallMissedSolid(),
VoiceCallOutgoingSolid(),
VoiceCallSolid(),
VolumeOff(),
VolumeOffSolid(),
@@ -852,10 +918,13 @@ object CompoundIcons {
Warning(),
WebBrowser(),
Windows(),
ZoomIn(),
ZoomOut(),
)
val allResIds get() = persistentListOf(
R.drawable.ic_compound_admin,
R.drawable.ic_compound_advanced_settings,
R.drawable.ic_compound_arrow_down,
R.drawable.ic_compound_arrow_left,
R.drawable.ic_compound_arrow_right,
@@ -869,6 +938,7 @@ object CompoundIcons {
R.drawable.ic_compound_backspace_solid,
R.drawable.ic_compound_block,
R.drawable.ic_compound_bold,
R.drawable.ic_compound_bug,
R.drawable.ic_compound_calendar,
R.drawable.ic_compound_chart,
R.drawable.ic_compound_chat,
@@ -1001,13 +1071,17 @@ object CompoundIcons {
R.drawable.ic_compound_qr_code,
R.drawable.ic_compound_quote,
R.drawable.ic_compound_raised_hand_solid,
R.drawable.ic_compound_re_order,
R.drawable.ic_compound_reaction,
R.drawable.ic_compound_reaction_add,
R.drawable.ic_compound_reaction_solid,
R.drawable.ic_compound_reply,
R.drawable.ic_compound_restart,
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_section,
R.drawable.ic_compound_send,
R.drawable.ic_compound_send_solid,
R.drawable.ic_compound_settings,
@@ -1026,14 +1100,19 @@ object CompoundIcons {
R.drawable.ic_compound_spotlight,
R.drawable.ic_compound_spotlight_view,
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_switch_camera_solid,
R.drawable.ic_compound_take_photo,
R.drawable.ic_compound_take_photo_solid,
R.drawable.ic_compound_text_formatting,
R.drawable.ic_compound_theme,
R.drawable.ic_compound_threads,
R.drawable.ic_compound_threads_solid,
R.drawable.ic_compound_time,
R.drawable.ic_compound_translate,
R.drawable.ic_compound_tree,
R.drawable.ic_compound_underline,
R.drawable.ic_compound_unknown,
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_off,
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_visibility_off,
R.drawable.ic_compound_visibility_on,
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_volume_off,
R.drawable.ic_compound_volume_off_solid,
@@ -1062,5 +1145,7 @@ object CompoundIcons {
R.drawable.ic_compound_warning,
R.drawable.ic_compound_web_browser,
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.
* Please see LICENSE files in the repository root for full details.
@@ -121,18 +121,14 @@ data class SemanticColors(
val gradientActionStop3: Color,
/** Background gradient stop for super and send buttons */
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 */
val gradientInfoStop1: Color,
/** Subtle background gradient stop for info */
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 */
val gradientSubtleStop1: Color,
/** 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.
* Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsDark = SemanticColors(
gradientActionStop2 = DarkColorTokens.colorGreen900,
gradientActionStop3 = DarkColorTokens.colorGreen700,
gradientActionStop4 = DarkColorTokens.colorGreen500,
gradientInfoStop1 = DarkColorTokens.colorAlphaBlue500,
gradientInfoStop2 = DarkColorTokens.colorAlphaBlue400,
gradientInfoStop3 = DarkColorTokens.colorAlphaBlue300,
gradientInfoStop4 = DarkColorTokens.colorAlphaBlue200,
gradientInfoStop5 = DarkColorTokens.colorAlphaBlue100,
gradientInfoStop6 = DarkColorTokens.colorTransparent,
gradientCriticalStop1 = DarkColorTokens.colorRed200,
gradientCriticalStop2 = DarkColorTokens.colorThemeBg,
gradientInfoStop1 = DarkColorTokens.colorBlue200,
gradientInfoStop2 = DarkColorTokens.colorThemeBg,
gradientSubtleStop1 = DarkColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = DarkColorTokens.colorAlphaGreen400,
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.
* Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsHcDark = SemanticColors(
gradientActionStop2 = DarkHcColorTokens.colorGreen900,
gradientActionStop3 = DarkHcColorTokens.colorGreen700,
gradientActionStop4 = DarkHcColorTokens.colorGreen500,
gradientInfoStop1 = DarkHcColorTokens.colorAlphaBlue500,
gradientInfoStop2 = DarkHcColorTokens.colorAlphaBlue400,
gradientInfoStop3 = DarkHcColorTokens.colorAlphaBlue300,
gradientInfoStop4 = DarkHcColorTokens.colorAlphaBlue200,
gradientInfoStop5 = DarkHcColorTokens.colorAlphaBlue100,
gradientInfoStop6 = DarkHcColorTokens.colorTransparent,
gradientCriticalStop1 = DarkHcColorTokens.colorRed200,
gradientCriticalStop2 = DarkHcColorTokens.colorThemeBg,
gradientInfoStop1 = DarkHcColorTokens.colorBlue200,
gradientInfoStop2 = DarkHcColorTokens.colorThemeBg,
gradientSubtleStop1 = DarkHcColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = DarkHcColorTokens.colorAlphaGreen400,
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.
* Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsLight = SemanticColors(
gradientActionStop2 = LightColorTokens.colorGreen700,
gradientActionStop3 = LightColorTokens.colorGreen900,
gradientActionStop4 = LightColorTokens.colorGreen1100,
gradientInfoStop1 = LightColorTokens.colorAlphaBlue500,
gradientInfoStop2 = LightColorTokens.colorAlphaBlue400,
gradientInfoStop3 = LightColorTokens.colorAlphaBlue300,
gradientInfoStop4 = LightColorTokens.colorAlphaBlue200,
gradientInfoStop5 = LightColorTokens.colorAlphaBlue100,
gradientInfoStop6 = LightColorTokens.colorTransparent,
gradientCriticalStop1 = LightColorTokens.colorRed200,
gradientCriticalStop2 = LightColorTokens.colorThemeBg,
gradientInfoStop1 = LightColorTokens.colorBlue200,
gradientInfoStop2 = LightColorTokens.colorThemeBg,
gradientSubtleStop1 = LightColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = LightColorTokens.colorAlphaGreen400,
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.
* Please see LICENSE files in the repository root for full details.
@@ -73,12 +73,10 @@ val compoundColorsHcLight = SemanticColors(
gradientActionStop2 = LightHcColorTokens.colorGreen700,
gradientActionStop3 = LightHcColorTokens.colorGreen900,
gradientActionStop4 = LightHcColorTokens.colorGreen1100,
gradientInfoStop1 = LightHcColorTokens.colorAlphaBlue500,
gradientInfoStop2 = LightHcColorTokens.colorAlphaBlue400,
gradientInfoStop3 = LightHcColorTokens.colorAlphaBlue300,
gradientInfoStop4 = LightHcColorTokens.colorAlphaBlue200,
gradientInfoStop5 = LightHcColorTokens.colorAlphaBlue100,
gradientInfoStop6 = LightHcColorTokens.colorTransparent,
gradientCriticalStop1 = LightHcColorTokens.colorRed200,
gradientCriticalStop2 = LightHcColorTokens.colorThemeBg,
gradientInfoStop1 = LightHcColorTokens.colorBlue200,
gradientInfoStop2 = LightHcColorTokens.colorThemeBg,
gradientSubtleStop1 = LightHcColorTokens.colorAlphaGreen500,
gradientSubtleStop2 = LightHcColorTokens.colorAlphaGreen400,
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.
* 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.
* 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.
* 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.
* 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.
* 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:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24H0z"/>
<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:fillColor="#FF000000"/>
</group>
<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:fillColor="#FF000000"/>
<path
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:fillType="evenOdd"/>
</vector>

View File

@@ -5,6 +5,7 @@
android:viewportWidth="24"
android:viewportHeight="24">
<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:fillColor="#FF000000"/>
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: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="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:viewportHeight="24">
<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: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,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:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24H0z"/>
<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.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>
<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:fillColor="#FF000000"
android:fillType="evenOdd"/>
</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 io.element.android.compound.theme.ElementTheme
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.AvatarData
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.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2392-6721
*/
@Composable
fun ComposerAlertMolecule(
avatar: AvatarData?,
content: AnnotatedString,
onSubmitClick: () -> Unit,
modifier: Modifier = Modifier,
level: ComposerAlertLevel = ComposerAlertLevel.Default,
level: ComposerAlertLevel = ComposerAlertLevel.Info,
showIcon: Boolean = false,
submitText: String = stringResource(CommonStrings.action_ok),
) {
@@ -52,20 +57,12 @@ fun ComposerAlertMolecule(
modifier.fillMaxWidth()
) {
val lineColor = when (level) {
ComposerAlertLevel.Default -> ElementTheme.colors.borderInfoSubtle
ComposerAlertLevel.Info -> ElementTheme.colors.borderInfoSubtle
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) {
ComposerAlertLevel.Default -> ElementTheme.colors.textPrimary
ComposerAlertLevel.Info -> ElementTheme.colors.textInfoPrimary
ComposerAlertLevel.Info -> ElementTheme.colors.textPrimary
ComposerAlertLevel.Critical -> ElementTheme.colors.textCriticalPrimary
}
@@ -75,12 +72,13 @@ fun ComposerAlertMolecule(
.height(1.dp)
.background(lineColor)
)
val brush = Brush.verticalGradient(
listOf(startColor, ElementTheme.colors.bgCanvasDefault),
)
val gradientColors = when (level) {
ComposerAlertLevel.Info -> gradientInfoColors()
ComposerAlertLevel.Critical -> gradientCriticalColors()
}
Box(
modifier = Modifier
.background(brush)
.background(Brush.verticalGradient(gradientColors))
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
) {
Column(
@@ -96,12 +94,10 @@ fun ComposerAlertMolecule(
)
} else if (showIcon) {
val icon = when (level) {
ComposerAlertLevel.Default -> CompoundIcons.Info()
ComposerAlertLevel.Info -> CompoundIcons.Info()
ComposerAlertLevel.Critical -> CompoundIcons.Error()
}
val iconTint = when (level) {
ComposerAlertLevel.Default -> ElementTheme.colors.iconPrimary
ComposerAlertLevel.Info -> ElementTheme.colors.iconInfoPrimary
ComposerAlertLevel.Critical -> ElementTheme.colors.iconCriticalPrimary
}
@@ -131,7 +127,6 @@ fun ComposerAlertMolecule(
}
enum class ComposerAlertLevel {
Default,
Info,
Critical
}

View File

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

View File

@@ -38,8 +38,11 @@ fun gradientSubtleColors(): List<Color> = listOf(
fun gradientInfoColors(): List<Color> = listOf(
ElementTheme.colors.gradientInfoStop1,
ElementTheme.colors.gradientInfoStop2,
ElementTheme.colors.gradientInfoStop3,
ElementTheme.colors.gradientInfoStop4,
ElementTheme.colors.gradientInfoStop5,
ElementTheme.colors.gradientInfoStop6,
)
@Composable
@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.
internal val iconsOther = listOf(
R.drawable.ic_notification,
R.drawable.ic_stop,
R.drawable.pin,
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()
/**
* The event was found but it has been redacted.
*/
data object EventRedacted : NotificationResolverException()
/**
* 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)
/**
* The device could not be created.
*/
class UnableToCreateDevice(message: String) : ErrorType(message)
/**
* An unknown error has happened.
*/
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
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -16,9 +17,20 @@ data class MediaSource(
/**
* Url of the media.
*/
val url: String,
private val url: String,
/**
* This is used to hold data for encrypted media.
*/
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
}
/**
* 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.
* @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.mxCallbackFlow
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.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
@@ -832,8 +832,8 @@ class RustMatrixClient(
if (workManagerScheduler.hasPendingWork(sessionId, WorkManagerRequestType.DB_VACUUM)) return
Timber.i("Scheduling periodic database vacuuming for session $sessionId")
val request = PerformDatabaseVacuumWorkManagerRequest(sessionId)
workManagerScheduler.submit(request)
val request = PerformDatabaseVacuumRequestBuilder(sessionId)
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.MissingSecretsBackup -> ErrorType.MissingSecretsBackup(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.UnsupportedProtocol -> ErrorType.UnsupportedProtocol(message.orEmpty())
}

View File

@@ -91,7 +91,7 @@ class RustMediaLoader(
return if (json != null) {
RustMediaSource.fromJson(json)
} 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")
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 -> {

View File

@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
internal class RustRoomListService(
@@ -33,6 +34,9 @@ internal class RustRoomListService(
private val roomSyncSubscriber: RoomSyncSubscriber,
private val sessionCoroutineScope: CoroutineScope,
) : RoomListService {
private val _isInitialSyncDone = AtomicBoolean(false)
override val isInitialSyncDone: Boolean get() = _isInitialSyncDone.get()
override fun createRoomList(
pageSize: Int,
source: RoomList.Source,
@@ -75,6 +79,9 @@ internal class RustRoomListService(
.map { it.toRoomListState() }
.onEach { state ->
Timber.d("RoomList state=$state")
if (state == RoomListService.State.Running) {
_isInitialSyncDone.set(true)
}
}
.distinctUntilChanged()
.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.TimelineListener
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 {
override fun onUpdate(status: RoomPaginationStatus) {
override fun onUpdate(status: PaginationStatus) {
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.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPaginationStatus
import uniffi.matrix_sdk.PaginationStatus
import java.io.File
import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@@ -147,8 +147,8 @@ class RustTimeline(
.onEach { backPaginationStatus ->
updatePaginationStatus(Timeline.PaginationDirection.BACKWARDS) {
when (backPaginationStatus) {
is RoomPaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitTimelineStart)
is RoomPaginationStatus.Paginating -> it.copy(isPaginating = true, hasMoreToLoad = true)
is PaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitTimelineStart)
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.Data
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkRequest
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.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.WorkManagerRequestWrapper
import io.element.android.libraries.workmanager.api.workManagerTag
import java.util.concurrent.TimeUnit
class PerformDatabaseVacuumWorkManagerRequest(
class PerformDatabaseVacuumRequestBuilder(
private val sessionId: SessionId,
) : WorkManagerRequest {
override fun build(): Result<List<WorkRequest>> {
) : WorkManagerRequestBuilder {
override suspend fun build(): Result<List<WorkManagerRequestWrapper>> {
val data = Data.Builder().putString(SESSION_ID_PARAM, sessionId.value).build()
val workRequest = PeriodicWorkRequest.Builder(
workerClass = VacuumDatabaseWorker::class,
@@ -41,6 +41,6 @@ class PerformDatabaseVacuumWorkManagerRequest(
)
.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.test.InMemorySessionStore
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.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@@ -33,7 +33,7 @@ import java.io.File
class RustMatrixClientFactoryTest {
@Test
fun test() = runTest {
val scheduleVacuumLambda = lambdaRecorder<WorkManagerRequest, Unit> {}
val scheduleVacuumLambda = lambdaRecorder<WorkManagerRequestBuilder, Unit> {}
val workManagerScheduler = FakeWorkManagerScheduler(submitLambda = scheduleVacuumLambda)
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.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
import uniffi.matrix_sdk.RoomPaginationStatus
import uniffi.matrix_sdk.PaginationStatus
class FakeFfiTimeline : Timeline(NoHandle) {
private var listener: TimelineListener? = null
@@ -33,7 +33,7 @@ class FakeFfiTimeline : Timeline(NoHandle) {
return FakeFfiTaskHandle()
}
fun emitPaginationStatus(status: RoomPaginationStatus) {
fun emitPaginationStatus(status: PaginationStatus) {
paginationStatusListener!!.onUpdate(status)
}

View File

@@ -32,7 +32,7 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.TimelineDiff
import uniffi.matrix_sdk.RoomPaginationStatus
import uniffi.matrix_sdk.PaginationStatus
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
class RustTimelineTest {
@@ -68,10 +68,10 @@ class RustTimelineTest {
// Start pagination
sut.paginate(Timeline.PaginationDirection.BACKWARDS)
// Simulate SDK starting pagination
inner.emitPaginationStatus(RoomPaginationStatus.Paginating)
inner.emitPaginationStatus(PaginationStatus.Paginating)
// No new events received
// 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
with(awaitItem()) {
assertThat(size).isEqualTo(2)

View File

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

View File

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

View File

@@ -25,5 +25,5 @@ internal class MediaRequestDataKeyer : Keyer<MediaRequestData> {
}
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) {
is MediaItem.DateSeparator -> Unit
is MediaItem.Event -> {
val sourceUrl = mediaItem.mediaSource().url
val sourceUrl = mediaItem.mediaSource().safeUrl
val localMedia = localMediaStates.getOrPut(sourceUrl) {
mutableStateOf(AsyncData.Uninitialized)
}
@@ -153,7 +153,7 @@ class MediaViewerDataSource(
}.toImmutableList()
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) {
@@ -162,7 +162,7 @@ class MediaViewerDataSource(
suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) {
Timber.d("loadMedia for ${data.eventId}")
val localMediaState = localMediaStates.getOrPut(data.mediaSource.url) {
val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) {
mutableStateOf(AsyncData.Uninitialized)
}
localMediaState.value = AsyncData.Loading()

View File

@@ -10,8 +10,8 @@ package io.element.android.libraries.permissions.impl.troubleshoot
import android.Manifest
import android.os.Build
import dev.zacsweers.metro.AppScope
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.impl.R
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.flow.StateFlow
@ContributesIntoSet(AppScope::class)
@ContributesIntoSet(SessionScope::class)
class NotificationTroubleshootCheckPermissionTest(
private val permissionStateProvider: PermissionStateProvider,
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 {
create("PushDatabase") {
schemaOutputDirectory = File("src/main/sqldelight/databases")
verifyMigrations = true
}
}
}

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